@newtonschool/react_proctoring_library 0.0.132 → 0.0.133-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ var _proctoringStatusUtils = require("../../utils/proctoringStatusUtils");
10
10
  var _permission = require("../../utils/permission");
11
11
  var _breachUtils = require("../../utils/breachUtils");
12
12
  var _usePageActivity = _interopRequireDefault(require("../../hooks/usePageActivity"));
13
+ var _proctoringDebugUtils = require("../../utils/proctoringDebugUtils");
13
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
15
  const SecondaryDevice = _ref => {
15
16
  let {
@@ -27,10 +28,36 @@ const SecondaryDevice = _ref => {
27
28
  const [secondaryDeviceOnline, setSecondaryDeviceOnline] = (0, _react.useState)(true);
28
29
  const [secondaryDeviceAllPermissionGranted, setSecondaryDeviceAllPermissionGranted] = (0, _react.useState)(true);
29
30
  const {
30
- isActive: isCurrentDeviceActive
31
+ isActive: isCurrentDeviceActive,
32
+ isVisible,
33
+ isFocused,
34
+ isLifecycleActive
31
35
  } = (0, _usePageActivity.default)();
32
36
  const allPermissionGranted = (0, _permission.hasAllPermissions)((0, _breachUtils.getRequiredPermissionsFromProctorParams)(proctorParams), permissions);
33
37
  const effectiveAllPermissionGranted = allPermissionGranted && isCurrentDeviceActive;
38
+ (0, _react.useEffect)(() => {
39
+ if (!isSecondaryDevice) {
40
+ return;
41
+ }
42
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_session_started', {
43
+ deviceType: 'secondary',
44
+ userUuid: (firebaseClient === null || firebaseClient === void 0 ? void 0 : firebaseClient.userUuid) || null,
45
+ requiredPermissions: (0, _breachUtils.getRequiredPermissionsFromProctorParams)(proctorParams),
46
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown'
47
+ }, firebaseClient);
48
+ }, [firebaseClient === null || firebaseClient === void 0 ? void 0 : firebaseClient.userUuid, isSecondaryDevice, proctorParams]);
49
+ (0, _react.useEffect)(() => {
50
+ if (!isSecondaryDevice) {
51
+ return;
52
+ }
53
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_page_activity_state_changed', {
54
+ deviceType: 'secondary',
55
+ isVisible,
56
+ isFocused,
57
+ isLifecycleActive,
58
+ isActive: isCurrentDeviceActive
59
+ }, firebaseClient);
60
+ }, [firebaseClient, isCurrentDeviceActive, isFocused, isLifecycleActive, isSecondaryDevice, isVisible]);
34
61
  (0, _react.useEffect)(() => {
35
62
  const cleanup = (0, _proctoringStatusUtils.setupDeviceStatusListeners)({
36
63
  firebaseClient,
@@ -20,6 +20,7 @@ var _webcamMicrophoneUtils = require("../../utils/webcamMicrophoneUtils");
20
20
  var _dom = require("../../constants/dom");
21
21
  var _useBreachData = require("../../hooks/useBreachData");
22
22
  var _proctoringStatusUtils = require("../../utils/proctoringStatusUtils");
23
+ var _proctoringDebugUtils = require("../../utils/proctoringDebugUtils");
23
24
  var _jsxRuntime = require("react/jsx-runtime");
24
25
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
25
26
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
@@ -46,6 +47,7 @@ const WebWebCam = _ref => {
46
47
  references,
47
48
  permissions
48
49
  } = (0, _react.useContext)(proctoredContext);
50
+ const deviceType = isSecondaryDevice ? 'secondary' : 'primary';
49
51
  const recurringFetchIntervalRef = (0, _react.useRef)(null);
50
52
  const {
51
53
  recurring = false,
@@ -108,6 +110,13 @@ const WebWebCam = _ref => {
108
110
  screenshotFormat: "image/jpeg",
109
111
  ref: webcamReference,
110
112
  onUserMedia: () => {
113
+ if (isSecondaryDevice) {
114
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_webcam_user_media_succeeded', {
115
+ deviceType,
116
+ retryCount,
117
+ userUuid: (firebaseClient === null || firebaseClient === void 0 ? void 0 : firebaseClient.userUuid) || null
118
+ }, firebaseClient);
119
+ }
111
120
  setVideoPermission(true);
112
121
  if (firebaseClient) {
113
122
  if (isSecondaryDevice) {
@@ -118,6 +127,17 @@ const WebWebCam = _ref => {
118
127
  }
119
128
  },
120
129
  onUserMediaError: err => {
130
+ if (isSecondaryDevice) {
131
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_webcam_user_media_failed', {
132
+ deviceType,
133
+ retryCount,
134
+ userUuid: (firebaseClient === null || firebaseClient === void 0 ? void 0 : firebaseClient.userUuid) || null,
135
+ error: {
136
+ name: (err === null || err === void 0 ? void 0 : err.name) || null,
137
+ message: (err === null || err === void 0 ? void 0 : err.message) || String(err)
138
+ }
139
+ }, firebaseClient);
140
+ }
121
141
  logger("Retry Count = ".concat(retryCount, " | Webcam error, onUserMediaError | ").concat(err.toString()));
122
142
  if (retryCount === 0) {
123
143
  setVideoPermission(false);
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ require("core-js/modules/es.symbol.description.js");
4
+ require("core-js/modules/esnext.iterator.constructor.js");
5
+ require("core-js/modules/esnext.iterator.filter.js");
6
+ require("core-js/modules/esnext.iterator.for-each.js");
7
+ Object.defineProperty(exports, "__esModule", {
8
+ value: true
9
+ });
10
+ exports.emitProctoringDebugLog = void 0;
11
+ require("core-js/modules/es.json.stringify.js");
12
+ require("core-js/modules/es.promise.js");
13
+ var _defaults = require("../constants/defaults");
14
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
15
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
16
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
17
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
18
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
19
+ const DEBUG_LOG_PREFIX = '::[secondary-device-debug]';
20
+ const WEB_REPORT_LOG_ENDPOINT = '/api/v1/user/report/web/';
21
+ const safeJsonStringify = value => {
22
+ try {
23
+ return JSON.stringify(value);
24
+ } catch (error) {
25
+ return JSON.stringify({
26
+ ts: Date.now(),
27
+ serializationError: (error === null || error === void 0 ? void 0 : error.message) || String(error)
28
+ });
29
+ }
30
+ };
31
+ const postDebugMessage = message => {
32
+ if (typeof window === 'undefined') {
33
+ return;
34
+ }
35
+ try {
36
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
37
+ const body = new Blob([safeJsonStringify({
38
+ message
39
+ })], {
40
+ type: 'application/json'
41
+ });
42
+ navigator.sendBeacon(WEB_REPORT_LOG_ENDPOINT, body);
43
+ } else if (typeof fetch === 'function') {
44
+ fetch(WEB_REPORT_LOG_ENDPOINT, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json'
48
+ },
49
+ body: safeJsonStringify({
50
+ message
51
+ }),
52
+ keepalive: true
53
+ }).catch(() => {});
54
+ }
55
+ } catch (_) {}
56
+ };
57
+ const isDebugLogEnabled = async firebaseClient => {
58
+ if (!(firebaseClient !== null && firebaseClient !== void 0 && firebaseClient.userUuid) || !(firebaseClient !== null && firebaseClient !== void 0 && firebaseClient.myFirebaseDB) || !(firebaseClient !== null && firebaseClient !== void 0 && firebaseClient.getRef) || !(firebaseClient !== null && firebaseClient !== void 0 && firebaseClient.getValue)) {
59
+ return false;
60
+ }
61
+ const {
62
+ getRef: ref,
63
+ getValue
64
+ } = firebaseClient;
65
+ const debugEnabledRef = ref(firebaseClient.myFirebaseDB, "".concat(_defaults.PROCTORING_STATUS_PATH, "/").concat(firebaseClient.userUuid, "/isDebugEnabled"));
66
+ try {
67
+ return await new Promise(resolve => {
68
+ getValue(debugEnabledRef, snapshot => {
69
+ var _snapshot$val, _snapshot$val2;
70
+ const value = (_snapshot$val = snapshot === null || snapshot === void 0 ? void 0 : (_snapshot$val2 = snapshot.val) === null || _snapshot$val2 === void 0 ? void 0 : _snapshot$val2.call(snapshot)) !== null && _snapshot$val !== void 0 ? _snapshot$val : snapshot;
71
+ resolve(Boolean(value));
72
+ }, () => resolve(false), {
73
+ onlyOnce: true
74
+ });
75
+ });
76
+ } catch (_) {
77
+ return false;
78
+ }
79
+ };
80
+ const emitProctoringDebugLog = exports.emitProctoringDebugLog = function emitProctoringDebugLog(event) {
81
+ let payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
82
+ let firebaseClient = arguments.length > 2 ? arguments[2] : undefined;
83
+ const message = "".concat(DEBUG_LOG_PREFIX, " ").concat(event, " ").concat(safeJsonStringify(_objectSpread({
84
+ ts: Date.now()
85
+ }, payload)));
86
+ isDebugLogEnabled(firebaseClient).then(enabled => {
87
+ if (!enabled) {
88
+ return;
89
+ }
90
+ postDebugMessage(message);
91
+ });
92
+ };
@@ -5,8 +5,28 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.updateCurrentDeviceStatus = exports.setupDeviceStatusListeners = exports.setSecondaryDeviceConnected = exports.setPrimaryDeviceConnected = void 0;
7
7
  var _defaults = require("../constants/defaults");
8
+ var _proctoringDebugUtils = require("./proctoringDebugUtils");
8
9
  const HEARTBEAT_INTERVAL_MS = 5000;
9
10
  const OTHER_DEVICE_HEARTBEAT_TIMEOUT_MS = 15000;
11
+ const getErrorPayload = error => ({
12
+ name: (error === null || error === void 0 ? void 0 : error.name) || null,
13
+ message: (error === null || error === void 0 ? void 0 : error.message) || String(error)
14
+ });
15
+ const getObservedStatusPayload = function getObservedStatusPayload(status) {
16
+ let now = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Date.now();
17
+ return {
18
+ connected: Boolean(status === null || status === void 0 ? void 0 : status.connected),
19
+ allPermissionGranted: typeof (status === null || status === void 0 ? void 0 : status.allPermissionGranted) === 'boolean' ? status.allPermissionGranted : null,
20
+ lastHeartbeatAt: typeof (status === null || status === void 0 ? void 0 : status.lastHeartbeatAt) === 'number' ? status.lastHeartbeatAt : null,
21
+ heartbeatAgeMs: typeof (status === null || status === void 0 ? void 0 : status.lastHeartbeatAt) === 'number' ? now - status.lastHeartbeatAt : null,
22
+ deviceId: (status === null || status === void 0 ? void 0 : status.deviceId) || null
23
+ };
24
+ };
25
+ const hasRelevantObservedChange = (previousStatus, nextStatus) => {
26
+ const previous = getObservedStatusPayload(previousStatus);
27
+ const next = getObservedStatusPayload(nextStatus);
28
+ return previous.connected !== next.connected || previous.allPermissionGranted !== next.allPermissionGranted || previous.deviceId !== next.deviceId;
29
+ };
10
30
 
11
31
  /**
12
32
  * Marks the primary device as connected in Firebase. Call when desktop webcam
@@ -27,7 +47,7 @@ const setPrimaryDeviceConnected = firebaseClient => {
27
47
  const payload = {
28
48
  connected: true
29
49
  };
30
- return update(primaryDeviceRef, payload).catch(error => {
50
+ return update(primaryDeviceRef, payload).then(() => {}).catch(error => {
31
51
  console.error('::[proctoringStatusUtils] Failed to set primary device status online:', error);
32
52
  });
33
53
  };
@@ -52,7 +72,13 @@ const setSecondaryDeviceConnected = firebaseClient => {
52
72
  const payload = {
53
73
  connected: true
54
74
  };
55
- return update(secondaryDeviceRef, payload).catch(error => {
75
+ return update(secondaryDeviceRef, payload).then(() => {}).catch(error => {
76
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_device_status_write_failed', {
77
+ deviceType: 'secondary',
78
+ userUuid: firebaseClient.userUuid,
79
+ payload,
80
+ error: getErrorPayload(error)
81
+ }, firebaseClient);
56
82
  console.error('::[proctoringStatusUtils] Failed to set secondary device status online:', error);
57
83
  });
58
84
  };
@@ -86,7 +112,16 @@ const updateCurrentDeviceStatus = _ref => {
86
112
  const payload = {
87
113
  allPermissionGranted
88
114
  };
89
- return update(deviceStatusRef, payload).catch(error => {
115
+ return update(deviceStatusRef, payload).then(() => {}).catch(error => {
116
+ if (!isSecondaryDevice) {
117
+ return;
118
+ }
119
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_device_status_write_failed', {
120
+ deviceType: 'secondary',
121
+ userUuid: firebaseClient.userUuid,
122
+ payload,
123
+ error: getErrorPayload(error)
124
+ }, firebaseClient);
90
125
  console.error('::[proctoringStatusUtils] Failed to update current device status:', error);
91
126
  });
92
127
  };
@@ -123,6 +158,7 @@ const setupDeviceStatusListeners = _ref2 => {
123
158
  onDisconnect,
124
159
  getValue: onValue
125
160
  } = firebaseClient || {};
161
+ const deviceType = isSecondaryDevice ? 'secondary' : 'primary';
126
162
  if (!userUuid || !firestoreAuthStatus || !ref || !set || !onDisconnect || !onValue) {
127
163
  return () => {};
128
164
  }
@@ -136,28 +172,52 @@ const setupDeviceStatusListeners = _ref2 => {
136
172
  // When this client disconnects (tab close, network loss)
137
173
  onDisconnect(currentDeviceStatusRef).update({
138
174
  connected: false
139
- }).catch(err => {
175
+ }).then(() => {}).catch(err => {
176
+ if (!isSecondaryDevice) {
177
+ return;
178
+ }
179
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_device_status_write_failed', {
180
+ deviceType,
181
+ userUuid,
182
+ payload: {
183
+ connected: false,
184
+ source: 'onDisconnect'
185
+ },
186
+ error: getErrorPayload(err)
187
+ }, firebaseClient);
140
188
  console.error('::[proctoringStatusUtils] Failed to mark current device disconnected:', err);
141
189
  });
142
190
 
143
191
  // Get initial values and update callbacks immediately
144
- const applyPrimarySnapshot = snapshot => {
192
+ const applyPrimarySnapshot = function applyPrimarySnapshot(snapshot) {
145
193
  var _snapshot$val, _snapshot$val2;
194
+ let source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'subscription';
195
+ const previousValue = latestPrimaryDeviceStatus;
146
196
  const val = (_snapshot$val = snapshot === null || snapshot === void 0 ? void 0 : (_snapshot$val2 = snapshot.val) === null || _snapshot$val2 === void 0 ? void 0 : _snapshot$val2.call(snapshot)) !== null && _snapshot$val !== void 0 ? _snapshot$val : snapshot;
147
197
  latestPrimaryDeviceStatus = val;
148
198
  onPrimaryDeviceStatusChange(Boolean(val === null || val === void 0 ? void 0 : val.connected));
149
199
  };
150
- const applySecondarySnapshot = snapshot => {
200
+ const applySecondarySnapshot = function applySecondarySnapshot(snapshot) {
151
201
  var _snapshot$val3, _snapshot$val4;
202
+ let source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'subscription';
203
+ const previousValue = latestSecondaryDeviceStatus;
152
204
  const val = (_snapshot$val3 = snapshot === null || snapshot === void 0 ? void 0 : (_snapshot$val4 = snapshot.val) === null || _snapshot$val4 === void 0 ? void 0 : _snapshot$val4.call(snapshot)) !== null && _snapshot$val3 !== void 0 ? _snapshot$val3 : snapshot;
153
205
  latestSecondaryDeviceStatus = val;
206
+ if (isSecondaryDevice && (source === 'initial_get' || hasRelevantObservedChange(previousValue, val))) {
207
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_device_status_observed', {
208
+ deviceType,
209
+ observedDeviceType: 'secondary',
210
+ source,
211
+ observedStatus: getObservedStatusPayload(val)
212
+ }, firebaseClient);
213
+ }
154
214
  onSecondaryDeviceStatusChange(Boolean(val === null || val === void 0 ? void 0 : val.connected), val === null || val === void 0 ? void 0 : val.allPermissionGranted);
155
215
  };
156
216
  if (typeof (primaryDeviceStatusRef === null || primaryDeviceStatusRef === void 0 ? void 0 : primaryDeviceStatusRef.get) === 'function') {
157
- primaryDeviceStatusRef.get().then(applyPrimarySnapshot).catch(() => {});
217
+ primaryDeviceStatusRef.get().then(snapshot => applyPrimarySnapshot(snapshot, 'initial_get')).catch(() => {});
158
218
  }
159
219
  if (typeof (secondaryDeviceStatusRef === null || secondaryDeviceStatusRef === void 0 ? void 0 : secondaryDeviceStatusRef.get) === 'function') {
160
- secondaryDeviceStatusRef.get().then(applySecondarySnapshot).catch(() => {});
220
+ secondaryDeviceStatusRef.get().then(snapshot => applySecondarySnapshot(snapshot, 'initial_get')).catch(() => {});
161
221
  }
162
222
 
163
223
  // Subscribe to updates
@@ -177,14 +237,48 @@ const setupDeviceStatusListeners = _ref2 => {
177
237
  update(currentDeviceStatusRef, {
178
238
  connected: true,
179
239
  lastHeartbeatAt: now
180
- }).catch(error => {
240
+ }).then(() => {}).catch(error => {
241
+ if (!isSecondaryDevice) {
242
+ return;
243
+ }
244
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_heartbeat_write_failed', {
245
+ deviceType,
246
+ userUuid,
247
+ isCurrentDeviceActive,
248
+ now,
249
+ isSecondaryDevice,
250
+ error: getErrorPayload(error)
251
+ }, firebaseClient);
181
252
  console.error('::[proctoringStatusUtils] Failed to send heartbeat for current device:', error);
182
253
  });
183
254
  }
184
255
  if (!isCurrentDeviceActive) {
185
256
  update(currentDeviceStatusRef, {
186
257
  connected: false
258
+ }).then(() => {
259
+ if (!isSecondaryDevice) {
260
+ return;
261
+ }
262
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_current_device_marked_inactive', {
263
+ deviceType,
264
+ userUuid,
265
+ now,
266
+ isSecondaryDevice
267
+ }, firebaseClient);
187
268
  }).catch(error => {
269
+ if (!isSecondaryDevice) {
270
+ return;
271
+ }
272
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_device_status_write_failed', {
273
+ deviceType,
274
+ userUuid,
275
+ now,
276
+ payload: {
277
+ connected: false,
278
+ source: 'inactive_page_state'
279
+ },
280
+ error: getErrorPayload(error)
281
+ }, firebaseClient);
188
282
  console.error('::[proctoringStatusUtils] Failed to mark current device inactive:', error);
189
283
  });
190
284
  }
@@ -196,7 +290,23 @@ const setupDeviceStatusListeners = _ref2 => {
196
290
  if (isConnected && typeof lastBeat === 'number' && now - lastBeat > OTHER_DEVICE_HEARTBEAT_TIMEOUT_MS) {
197
291
  update(otherDeviceStatusRef, {
198
292
  connected: false
199
- }).catch(otherErr => {
293
+ }).then(() => {}).catch(otherErr => {
294
+ const isMarkingSecondaryDeviceStale = !isSecondaryDevice;
295
+ if (!isMarkingSecondaryDeviceStale) {
296
+ return;
297
+ }
298
+ (0, _proctoringDebugUtils.emitProctoringDebugLog)('secondary_device_status_write_failed', {
299
+ deviceType: 'secondary',
300
+ userUuid,
301
+ now,
302
+ payload: {
303
+ connected: false,
304
+ source: 'stale_other_device',
305
+ otherDeviceType: 'secondary',
306
+ otherDeviceStatus: getObservedStatusPayload(otherDeviceStatus, now)
307
+ },
308
+ error: getErrorPayload(otherErr)
309
+ }, firebaseClient);
200
310
  console.error('::[proctoringStatusUtils] Failed to mark other device disconnected due to stale heartbeat:', otherErr);
201
311
  });
202
312
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newtonschool/react_proctoring_library",
3
- "version": "0.0.132",
3
+ "version": "0.0.133-beta.0",
4
4
  "description": "Used to proctor online tests",
5
5
  "author": "ayushkagrawal,shreyachandra,weastel",
6
6
  "main": "dist/index.js",