您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

CallStats.js 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. /* global $, callstats */
  2. const logger = require('jitsi-meet-logger').getLogger(__filename);
  3. const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
  4. const jsSHA = require('jssha');
  5. const io = require('socket.io-client');
  6. /**
  7. * We define enumeration of wrtcFuncNames as we need them before
  8. * callstats is initialized to queue events.
  9. * @const
  10. * @see http://www.callstats.io/api/#enumeration-of-wrtcfuncnames
  11. */
  12. const wrtcFuncNames = {
  13. createOffer: 'createOffer',
  14. createAnswer: 'createAnswer',
  15. setLocalDescription: 'setLocalDescription',
  16. setRemoteDescription: 'setRemoteDescription',
  17. addIceCandidate: 'addIceCandidate',
  18. getUserMedia: 'getUserMedia',
  19. iceConnectionFailure: 'iceConnectionFailure',
  20. signalingError: 'signalingError',
  21. applicationLog: 'applicationLog'
  22. };
  23. /**
  24. * We define enumeration of fabricEvent as we need them before
  25. * callstats is initialized to queue events.
  26. * @const
  27. * @see http://www.callstats.io/api/#enumeration-of-fabricevent
  28. */
  29. const fabricEvent = {
  30. fabricHold: 'fabricHold',
  31. fabricResume: 'fabricResume',
  32. audioMute: 'audioMute',
  33. audioUnmute: 'audioUnmute',
  34. videoPause: 'videoPause',
  35. videoResume: 'videoResume',
  36. fabricUsageEvent: 'fabricUsageEvent',
  37. fabricStats: 'fabricStats',
  38. fabricTerminated: 'fabricTerminated',
  39. screenShareStart: 'screenShareStart',
  40. screenShareStop: 'screenShareStop',
  41. dominantSpeaker: 'dominantSpeaker',
  42. activeDeviceList: 'activeDeviceList'
  43. };
  44. /**
  45. * The user id to report to callstats as destination.
  46. * @type {string}
  47. */
  48. const DEFAULT_REMOTE_USER = 'jitsi';
  49. /**
  50. * Type of pending reports, can be event or an error.
  51. * @type {{ERROR: string, EVENT: string}}
  52. */
  53. const reportType = {
  54. ERROR: 'error',
  55. EVENT: 'event',
  56. MST_WITH_USERID: 'mstWithUserID'
  57. };
  58. /**
  59. * Set of currently existing {@link CallStats} instances.
  60. * @type {Set<CallStats>}
  61. */
  62. let _fabrics;
  63. /**
  64. * An instance of this class is a wrapper for the CallStats API fabric. A fabric
  65. * reports one peer connection the the CallStats backend and is allocated with
  66. * {@link callstats.addNewFabric}. It has a bunch of instance methods for
  67. * reporting various events. A fabric is considered disposed when
  68. * {@link CallStats.sendTerminateEvent} is executed.
  69. *
  70. * Currently only one backend instance can be created ever and it's done using
  71. * {@link CallStats.initBackend}. At the time of this writing there is no way to
  72. * explicitly shutdown the backend, but it's supposed to close it's connection
  73. * automatically, after all fabrics have been terminated.
  74. */
  75. export default class CallStats {
  76. /**
  77. * A callback passed to {@link callstats.addNewFabric}.
  78. * @param {string} error 'success' means ok
  79. * @param {string} msg some more details
  80. * @private
  81. */
  82. static _addNewFabricCallback(error, msg) {
  83. if (CallStats.backend && error !== 'success') {
  84. logger.error(`Monitoring status: ${error} msg: ${msg}`);
  85. }
  86. }
  87. /**
  88. * Callback passed to {@link callstats.initialize} (backend initialization)
  89. * @param {string} error 'success' means ok
  90. * @param {String} msg
  91. * @private
  92. */
  93. static _initCallback(error, msg) {
  94. logger.log(`CallStats Status: err=${error} msg=${msg}`);
  95. // there is no lib, nothing to report to
  96. if (error !== 'success') {
  97. return;
  98. }
  99. // I hate that
  100. let atLeastOneFabric = false;
  101. let defaultInstance = null;
  102. for (const callStatsInstance of CallStats.fabrics.values()) {
  103. if (!callStatsInstance.hasFabric) {
  104. logger.debug('addNewFabric - initCallback');
  105. if (callStatsInstance._addNewFabric()) {
  106. atLeastOneFabric = true;
  107. if (!defaultInstance) {
  108. defaultInstance = callStatsInstance;
  109. }
  110. }
  111. }
  112. }
  113. if (!atLeastOneFabric) {
  114. return;
  115. }
  116. CallStats.initialized = true;
  117. // There is no conference ID nor a PeerConnection available when some of
  118. // the events are scheduled on the reportsQueue, so those will be
  119. // reported on the first initialized fabric.
  120. const defaultConfID = defaultInstance.confID;
  121. const defaultPC = defaultInstance.peerconnection;
  122. // notify callstats about failures if there were any
  123. for (const report of CallStats.reportsQueue) {
  124. if (report.type === reportType.ERROR) {
  125. const errorData = report.data;
  126. CallStats._reportError(
  127. defaultInstance,
  128. errorData.type,
  129. errorData.error,
  130. errorData.pc || defaultPC);
  131. } else if (report.type === reportType.EVENT) {
  132. // if we have and event to report and we failed to add
  133. // fabric this event will not be reported anyway, returning
  134. // an error
  135. const eventData = report.data;
  136. CallStats.backend.sendFabricEvent(
  137. report.pc || defaultPC,
  138. eventData.event,
  139. defaultConfID,
  140. eventData.eventData);
  141. } else if (report.type === reportType.MST_WITH_USERID) {
  142. const data = report.data;
  143. CallStats.backend.associateMstWithUserID(
  144. report.pc || defaultPC,
  145. data.callStatsId,
  146. defaultConfID,
  147. data.ssrc,
  148. data.usageLabel,
  149. data.containerId
  150. );
  151. }
  152. }
  153. CallStats.reportsQueue.length = 0;
  154. }
  155. /* eslint-disable max-params */
  156. /**
  157. * Reports an error to callstats.
  158. *
  159. * @param {CallStats} [cs]
  160. * @param type the type of the error, which will be one of the wrtcFuncNames
  161. * @param error the error
  162. * @param pc the peerconnection
  163. * @private
  164. */
  165. static _reportError(cs, type, error, pc) {
  166. let _error = error;
  167. if (!_error) {
  168. logger.warn('No error is passed!');
  169. _error = new Error('Unknown error');
  170. }
  171. if (CallStats.initialized && cs) {
  172. CallStats.backend.reportError(pc, cs.confID, type, _error);
  173. } else {
  174. CallStats.reportsQueue.push({
  175. type: reportType.ERROR,
  176. data: {
  177. error: _error,
  178. pc,
  179. type
  180. }
  181. });
  182. }
  183. // else just ignore it
  184. }
  185. /* eslint-enable max-params */
  186. /**
  187. * Reports an error to callstats.
  188. *
  189. * @param {CallStats} cs
  190. * @param event the type of the event, which will be one of the fabricEvent
  191. * @param eventData additional data to pass to event
  192. * @private
  193. */
  194. static _reportEvent(cs, event, eventData) {
  195. const pc = cs && cs.peerconnection;
  196. const confID = cs && cs.confID;
  197. if (CallStats.initialized && cs) {
  198. CallStats.backend.sendFabricEvent(pc, event, confID, eventData);
  199. } else {
  200. CallStats.reportsQueue.push({
  201. confID,
  202. pc,
  203. type: reportType.EVENT,
  204. data: { event,
  205. eventData }
  206. });
  207. }
  208. }
  209. /**
  210. * Wraps some of the CallStats API method and logs their calls with
  211. * arguments on the debug logging level. Also wraps some of the backend
  212. * methods execution into try catch blocks to not crash the app in case
  213. * there is a problem with the backend itself.
  214. * @param {callstats} theBackend
  215. * @private
  216. */
  217. static _traceAndCatchBackendCalls(theBackend) {
  218. const tryCatchMethods = [
  219. 'associateMstWithUserID',
  220. 'sendFabricEvent',
  221. 'sendUserFeedback'
  222. // 'reportError', - this one needs special handling - see code below
  223. ];
  224. for (const methodName of tryCatchMethods) {
  225. const originalMethod = theBackend[methodName];
  226. theBackend[methodName] = function(...theArguments) {
  227. try {
  228. return originalMethod.apply(theBackend, theArguments);
  229. } catch (e) {
  230. GlobalOnErrorHandler.callErrorHandler(e);
  231. }
  232. };
  233. }
  234. const debugMethods = [
  235. 'associateMstWithUserID',
  236. 'sendFabricEvent',
  237. 'sendUserFeedback'
  238. // 'reportError', - this one needs special handling - see code below
  239. ];
  240. for (const methodName of debugMethods) {
  241. const originalMethod = theBackend[methodName];
  242. theBackend[methodName] = function(...theArguments) {
  243. logger.debug(methodName, theArguments);
  244. originalMethod.apply(theBackend, theArguments);
  245. };
  246. }
  247. const originalReportError = theBackend.reportError;
  248. /* eslint-disable max-params */
  249. theBackend.reportError
  250. = function(pc, cs, type, ...args) {
  251. // Logs from the logger are submitted on the applicationLog event
  252. // "type". Logging the arguments on the logger will create endless
  253. // loop, because it will put all the logs to the logger queue again.
  254. if (type === wrtcFuncNames.applicationLog) {
  255. // NOTE otherArguments are not logged to the console on purpose
  256. // to not log the whole log batch
  257. // FIXME check the current logging level (currently not exposed
  258. // by the logger implementation)
  259. console && console.debug('reportError', pc, cs, type);
  260. } else {
  261. logger.debug('reportError', pc, cs, type, ...args);
  262. }
  263. try {
  264. originalReportError.call(theBackend, pc, cs, type, ...args);
  265. } catch (exception) {
  266. if (type === wrtcFuncNames.applicationLog) {
  267. console && console.error('reportError', exception);
  268. } else {
  269. GlobalOnErrorHandler.callErrorHandler(exception);
  270. }
  271. }
  272. };
  273. /* eslint-enable max-params */
  274. }
  275. /**
  276. * Returns the Set with the currently existing {@link CallStats} instances.
  277. * Lazily initializes the Set to allow any Set polyfills to be applied.
  278. * @type {Set<CallStats>}
  279. */
  280. static get fabrics() {
  281. if (!_fabrics) {
  282. _fabrics = new Set();
  283. }
  284. return _fabrics;
  285. }
  286. /**
  287. * Initializes the CallStats backend. Should be called only if
  288. * {@link CallStats.isBackendInitialized} returns <tt>false</tt>.
  289. * @param {object} options
  290. * @param {String} options.callStatsID CallStats credentials - ID
  291. * @param {String} options.callStatsSecret CallStats credentials - secret
  292. * @param {string} options.aliasName the <tt>aliasName</tt> part of
  293. * the <tt>userID</tt> aka endpoint ID, see CallStats docs for more info.
  294. * @param {string} options.userName the <tt>userName</tt> part of
  295. * the <tt>userID</tt> aka display name, see CallStats docs for more info.
  296. *
  297. */
  298. static initBackend(options) {
  299. if (CallStats.backend) {
  300. throw new Error('CallStats backend has been initialized already!');
  301. }
  302. try {
  303. CallStats.backend
  304. = new callstats($, io, jsSHA); // eslint-disable-line new-cap
  305. CallStats._traceAndCatchBackendCalls(CallStats.backend);
  306. CallStats.userID = {
  307. aliasName: options.aliasName,
  308. userName: options.userName
  309. };
  310. CallStats.callStatsID = options.callStatsID;
  311. CallStats.callStatsSecret = options.callStatsSecret;
  312. // userID is generated or given by the origin server
  313. CallStats.backend.initialize(
  314. CallStats.callStatsID,
  315. CallStats.callStatsSecret,
  316. CallStats.userID,
  317. CallStats._initCallback);
  318. return true;
  319. } catch (e) {
  320. // The callstats.io API failed to initialize (e.g. because its
  321. // download did not succeed in general or on time). Further attempts
  322. // to utilize it cannot possibly succeed.
  323. GlobalOnErrorHandler.callErrorHandler(e);
  324. CallStats.backend = null;
  325. logger.error(e);
  326. return false;
  327. }
  328. }
  329. /**
  330. * Checks if the CallStats backend has been created. It does not mean that
  331. * it has been initialized, but only that the API instance has been
  332. * allocated successfully.
  333. * @return {boolean} <tt>true</tt> if backend exists or <tt>false</tt>
  334. * otherwise
  335. */
  336. static isBackendInitialized() {
  337. return Boolean(CallStats.backend);
  338. }
  339. /**
  340. * Notifies CallStats about active device.
  341. * @param {{deviceList: {String:String}}} devicesData list of devices with
  342. * their data
  343. * @param {CallStats} cs callstats instance related to the event
  344. */
  345. static sendActiveDeviceListEvent(devicesData, cs) {
  346. CallStats._reportEvent(cs, fabricEvent.activeDeviceList, devicesData);
  347. }
  348. /**
  349. * Notifies CallStats that there is a log we want to report.
  350. *
  351. * @param {Error} e error to send or {String} message
  352. * @param {CallStats} cs callstats instance related to the error (optional)
  353. */
  354. static sendApplicationLog(e, cs) {
  355. try {
  356. CallStats._reportError(
  357. cs,
  358. wrtcFuncNames.applicationLog,
  359. e,
  360. cs && cs.peerconnection);
  361. } catch (error) {
  362. // If sendApplicationLog fails it should not be printed to
  363. // the logger, because it will try to push the logs again
  364. // (through sendApplicationLog) and an endless loop is created.
  365. if (console && (typeof console.error === 'function')) {
  366. // FIXME send analytics event as well
  367. console.error('sendApplicationLog failed', error);
  368. }
  369. }
  370. }
  371. /**
  372. * Sends the given feedback through CallStats.
  373. *
  374. * @param {string} conferenceID the conference ID for which the feedback
  375. * will be reported.
  376. * @param overallFeedback an integer between 1 and 5 indicating the
  377. * user feedback
  378. * @param detailedFeedback detailed feedback from the user. Not yet used
  379. */
  380. static sendFeedback(conferenceID, overallFeedback, detailedFeedback) {
  381. if (CallStats.backend) {
  382. CallStats.backend.sendUserFeedback(
  383. conferenceID, {
  384. userID: CallStats.userID,
  385. overall: overallFeedback,
  386. comment: detailedFeedback
  387. });
  388. } else {
  389. logger.error('Failed to submit feedback to CallStats - no backend');
  390. }
  391. }
  392. /**
  393. * Notifies CallStats that getUserMedia failed.
  394. *
  395. * @param {Error} e error to send
  396. * @param {CallStats} cs callstats instance related to the error (optional)
  397. */
  398. static sendGetUserMediaFailed(e, cs) {
  399. CallStats._reportError(cs, wrtcFuncNames.getUserMedia, e, null);
  400. }
  401. /**
  402. * Notifies CallStats for mute events
  403. * @param mute {boolean} true for muted and false for not muted
  404. * @param type {String} "audio"/"video"
  405. * @param {CallStats} cs callstats instance related to the event
  406. */
  407. static sendMuteEvent(mute, type, cs) {
  408. let event;
  409. if (type === 'video') {
  410. event = mute ? fabricEvent.videoPause : fabricEvent.videoResume;
  411. } else {
  412. event = mute ? fabricEvent.audioMute : fabricEvent.audioUnmute;
  413. }
  414. CallStats._reportEvent(cs, event);
  415. }
  416. /**
  417. * Creates new CallStats instance that handles all callstats API calls for
  418. * given {@link TraceablePeerConnection}. Each instance is meant to handle
  419. * one CallStats fabric added with 'addFabric' API method for the
  420. * {@link TraceablePeerConnection} instance passed in the constructor.
  421. * @param {TraceablePeerConnection} tpc
  422. * @param {Object} options
  423. * @param {string} options.confID the conference ID that wil be used to
  424. * report the session.
  425. * @param {string} [options.remoteUserID='jitsi'] the remote user ID to
  426. * which given <tt>tpc</tt> is connected.
  427. */
  428. constructor(tpc, options) {
  429. if (!CallStats.backend) {
  430. throw new Error('CallStats backend not intiialized!');
  431. }
  432. this.confID = options.confID;
  433. this.tpc = tpc;
  434. this.peerconnection = tpc.peerconnection;
  435. this.remoteUserID = options.remoteUserID || DEFAULT_REMOTE_USER;
  436. this.hasFabric = false;
  437. CallStats.fabrics.add(this);
  438. if (CallStats.initialized) {
  439. this._addNewFabric();
  440. }
  441. }
  442. /**
  443. * Initializes CallStats fabric by calling "addNewFabric" for
  444. * the peer connection associated with this instance.
  445. * @return {boolean} true if the call was successful or false otherwise.
  446. */
  447. _addNewFabric() {
  448. logger.info('addNewFabric', this.remoteUserID, this);
  449. try {
  450. const ret
  451. = CallStats.backend.addNewFabric(
  452. this.peerconnection,
  453. this.remoteUserID,
  454. CallStats.backend.fabricUsage.multiplex,
  455. this.confID,
  456. CallStats._addNewFabricCallback);
  457. this.hasFabric = true;
  458. const success = ret.status === 'success';
  459. if (!success) {
  460. logger.error('callstats fabric not initilized', ret.message);
  461. }
  462. return success;
  463. } catch (error) {
  464. GlobalOnErrorHandler.callErrorHandler(error);
  465. return false;
  466. }
  467. }
  468. /* eslint-disable max-params */
  469. /**
  470. * Lets CallStats module know where is given SSRC rendered by providing
  471. * renderer tag ID.
  472. * If the lib is not initialized yet queue the call for later, when it's
  473. * ready.
  474. * @param {number} ssrc the SSRC of the stream
  475. * @param {boolean} isLocal indicates whether this the stream is local
  476. * @param {string|null} streamEndpointId if the stream is not local the it
  477. * needs to contain the stream owner's ID
  478. * @param {string} usageLabel meaningful usage label of this stream like
  479. * 'microphone', 'camera' or 'screen'.
  480. * @param {string} containerId the id of media 'audio' or 'video' tag which
  481. * renders the stream.
  482. */
  483. associateStreamWithVideoTag(
  484. ssrc,
  485. isLocal,
  486. streamEndpointId,
  487. usageLabel,
  488. containerId) {
  489. if (!CallStats.backend) {
  490. return;
  491. }
  492. const callStatsId = isLocal ? CallStats.userID : streamEndpointId;
  493. if (CallStats.initialized) {
  494. CallStats.backend.associateMstWithUserID(
  495. this.peerconnection,
  496. callStatsId,
  497. this.confID,
  498. ssrc,
  499. usageLabel,
  500. containerId);
  501. } else {
  502. CallStats.reportsQueue.push({
  503. type: reportType.MST_WITH_USERID,
  504. pc: this.peerconnection,
  505. data: {
  506. callStatsId,
  507. containerId,
  508. ssrc,
  509. usageLabel
  510. }
  511. });
  512. }
  513. }
  514. /* eslint-enable max-params */
  515. /**
  516. * Notifies CallStats that we are the new dominant speaker in the
  517. * conference.
  518. */
  519. sendDominantSpeakerEvent() {
  520. CallStats._reportEvent(this, fabricEvent.dominantSpeaker);
  521. }
  522. /**
  523. * Notifies CallStats that the fabric for the underlying peerconnection was
  524. * closed and no evens should be reported, after this call.
  525. */
  526. sendTerminateEvent() {
  527. if (CallStats.initialized) {
  528. CallStats.backend.sendFabricEvent(
  529. this.peerconnection,
  530. CallStats.backend.fabricEvent.fabricTerminated,
  531. this.confID);
  532. }
  533. CallStats.fabrics.delete(this);
  534. }
  535. /**
  536. * Notifies CallStats for ice connection failed
  537. */
  538. sendIceConnectionFailedEvent() {
  539. CallStats._reportError(
  540. this,
  541. wrtcFuncNames.iceConnectionFailure,
  542. null,
  543. this.peerconnection);
  544. }
  545. /**
  546. * Notifies CallStats that peer connection failed to create offer.
  547. *
  548. * @param {Error} e error to send
  549. */
  550. sendCreateOfferFailed(e) {
  551. CallStats._reportError(
  552. this, wrtcFuncNames.createOffer, e, this.peerconnection);
  553. }
  554. /**
  555. * Notifies CallStats that peer connection failed to create answer.
  556. *
  557. * @param {Error} e error to send
  558. */
  559. sendCreateAnswerFailed(e) {
  560. CallStats._reportError(
  561. this, wrtcFuncNames.createAnswer, e, this.peerconnection);
  562. }
  563. /**
  564. * Sends either resume or hold event for the fabric associated with
  565. * the underlying peerconnection.
  566. * @param {boolean} isResume true to resume or false to hold
  567. */
  568. sendResumeOrHoldEvent(isResume) {
  569. CallStats._reportEvent(
  570. this,
  571. isResume ? fabricEvent.fabricResume : fabricEvent.fabricHold);
  572. }
  573. /**
  574. * Notifies CallStats for screen sharing events
  575. * @param {boolean} start true for starting screen sharing and
  576. * false for not stopping
  577. */
  578. sendScreenSharingEvent(start) {
  579. CallStats._reportEvent(
  580. this,
  581. start ? fabricEvent.screenShareStart : fabricEvent.screenShareStop);
  582. }
  583. /**
  584. * Notifies CallStats that peer connection failed to set local description.
  585. *
  586. * @param {Error} e error to send
  587. */
  588. sendSetLocalDescFailed(e) {
  589. CallStats._reportError(
  590. this, wrtcFuncNames.setLocalDescription, e, this.peerconnection);
  591. }
  592. /**
  593. * Notifies CallStats that peer connection failed to set remote description.
  594. *
  595. * @param {Error} e error to send
  596. */
  597. sendSetRemoteDescFailed(e) {
  598. CallStats._reportError(
  599. this, wrtcFuncNames.setRemoteDescription, e, this.peerconnection);
  600. }
  601. /**
  602. * Notifies CallStats that peer connection failed to add ICE candidate.
  603. *
  604. * @param {Error} e error to send
  605. */
  606. sendAddIceCandidateFailed(e) {
  607. CallStats._reportError(
  608. this, wrtcFuncNames.addIceCandidate, e, this.peerconnection);
  609. }
  610. }
  611. /**
  612. * The CallStats API backend instance
  613. * @type {callstats}
  614. */
  615. CallStats.backend = null;
  616. // some errors/events may happen before CallStats init
  617. // in this case we accumulate them in this array
  618. // and send them to callstats on init
  619. CallStats.reportsQueue = [];
  620. /**
  621. * Whether the library was successfully initialized using its initialize method.
  622. * And whether we had successfully called addNewFabric at least once.
  623. * @type {boolean}
  624. */
  625. CallStats.initialized = false;
  626. /**
  627. * Part of the CallStats credentials - application ID
  628. * @type {string}
  629. */
  630. CallStats.callStatsID = null;
  631. /**
  632. * Part of the CallStats credentials - application secret
  633. * @type {string}
  634. */
  635. CallStats.callStatsSecret = null;
  636. /**
  637. * Local CallStats user ID structure. Can be set only once when
  638. * {@link backend} is initialized, so it's static for the time being.
  639. * See CallStats API for more info:
  640. * https://www.callstats.io/api/#userid
  641. * @type {object}
  642. */
  643. CallStats.userID = null;