@atlaskit/collab-provider 9.7.1 → 9.7.3

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/channel.js +107 -4
  3. package/dist/cjs/errors/error-code-mapper.js +7 -1
  4. package/dist/cjs/errors/error-types.js +5 -0
  5. package/dist/cjs/feature-flags/__test__/index.unit.js +3 -2
  6. package/dist/cjs/feature-flags/index.js +4 -2
  7. package/dist/cjs/helpers/const.js +2 -1
  8. package/dist/cjs/helpers/socket-message-metrics.js +54 -0
  9. package/dist/cjs/version-wrapper.js +1 -1
  10. package/dist/cjs/version.json +1 -1
  11. package/dist/es2019/channel.js +77 -4
  12. package/dist/es2019/errors/error-code-mapper.js +7 -1
  13. package/dist/es2019/errors/error-types.js +5 -0
  14. package/dist/es2019/feature-flags/__test__/index.unit.js +3 -2
  15. package/dist/es2019/feature-flags/index.js +4 -2
  16. package/dist/es2019/helpers/const.js +2 -1
  17. package/dist/es2019/helpers/socket-message-metrics.js +40 -0
  18. package/dist/es2019/version-wrapper.js +1 -1
  19. package/dist/es2019/version.json +1 -1
  20. package/dist/esm/channel.js +108 -5
  21. package/dist/esm/errors/error-code-mapper.js +7 -1
  22. package/dist/esm/errors/error-types.js +5 -0
  23. package/dist/esm/feature-flags/__test__/index.unit.js +3 -2
  24. package/dist/esm/feature-flags/index.js +4 -2
  25. package/dist/esm/helpers/const.js +2 -1
  26. package/dist/esm/helpers/socket-message-metrics.js +45 -0
  27. package/dist/esm/version-wrapper.js +1 -1
  28. package/dist/esm/version.json +1 -1
  29. package/dist/types/channel.d.ts +13 -2
  30. package/dist/types/errors/error-types.d.ts +20 -2
  31. package/dist/types/feature-flags/types.d.ts +1 -0
  32. package/dist/types/helpers/const.d.ts +12 -2
  33. package/dist/types/helpers/socket-message-metrics.d.ts +14 -0
  34. package/dist/types/types.d.ts +9 -0
  35. package/dist/types-ts4.5/channel.d.ts +13 -2
  36. package/dist/types-ts4.5/errors/error-types.d.ts +20 -2
  37. package/dist/types-ts4.5/feature-flags/types.d.ts +1 -0
  38. package/dist/types-ts4.5/helpers/const.d.ts +12 -2
  39. package/dist/types-ts4.5/helpers/socket-message-metrics.d.ts +14 -0
  40. package/dist/types-ts4.5/types.d.ts +9 -0
  41. package/package.json +2 -2
  42. package/report.api.md +7 -0
  43. package/tmp/api-report-tmp.d.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @atlaskit/collab-provider
2
2
 
3
+ ## 9.7.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`04fa8eb5246`](https://bitbucket.org/atlassian/atlassian-frontend/commits/04fa8eb5246) - Added rate limiting options to collab provider
8
+
9
+ ## 9.7.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [`f9735e0690e`](https://bitbucket.org/atlassian/atlassian-frontend/commits/f9735e0690e) - Using socket.onAnyOutgoing to measure and send message metrics
14
+
3
15
  ## 9.7.1
4
16
 
5
17
  ### Patch Changes
@@ -24,6 +24,11 @@ var _ufo = require("./analytics/ufo");
24
24
  var _disconnectedReasonMapper = require("./disconnected-reason-mapper");
25
25
  var _network = _interopRequireDefault(require("./connectivity/network"));
26
26
  var _errorTypes = require("./errors/error-types");
27
+ var _socketMessageMetrics = require("./helpers/socket-message-metrics");
28
+ var _featureFlags = require("./feature-flags");
29
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
30
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
31
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
27
32
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
28
33
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
29
34
  function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }
@@ -36,11 +41,19 @@ var Channel = /*#__PURE__*/function (_Emitter) {
36
41
  var _this;
37
42
  (0, _classCallCheck2.default)(this, Channel);
38
43
  _this = _super.call(this);
44
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "RATE_LIMIT_TYPE_NONE", 0);
45
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "RATE_LIMIT_TYPE_SOFT", 1);
46
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "RATE_LIMIT_TYPE_HARD", 2);
39
47
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "connected", false);
40
48
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "socket", null);
41
49
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "reconnectHelper", null);
42
50
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "initialized", false);
43
51
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "network", null);
52
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "rateLimitWindowDurationMs", 60000);
53
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "rateLimitWindowStartMs", 0);
54
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "stepCounter", 0);
55
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "stepSizeCounter", 0);
56
+ (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "maxStepSize", 0);
44
57
  // read-only getters used for tests
45
58
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "getInitialized", function () {
46
59
  return _this.initialized;
@@ -70,7 +83,7 @@ var Channel = /*#__PURE__*/function (_Emitter) {
70
83
  reason: data.reason,
71
84
  // Potentially incorrect when value of token changes between connecting and connect.
72
85
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
73
- usedCachedToken: _this.token ? true : false
86
+ usedCachedToken: !!_this.token
74
87
  });
75
88
  _this.unsetToken();
76
89
  });
@@ -81,7 +94,7 @@ var Channel = /*#__PURE__*/function (_Emitter) {
81
94
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
82
95
  // Potentially incorrect when value of token changes between connecting and connect.
83
96
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
84
- usedCachedToken: _this.token ? true : false
97
+ usedCachedToken: !!_this.token
85
98
  });
86
99
  (_this$analyticsHelper3 = _this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Error while establishing connection');
87
100
  // If error received with `data`, it means the connection is rejected
@@ -125,6 +138,9 @@ var Channel = /*#__PURE__*/function (_Emitter) {
125
138
  });
126
139
  (0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onConnect", function () {
127
140
  var _this$analyticsHelper5;
141
+ if ((0, _featureFlags.getCollabProviderFeatureFlag)('socketMessageMetricsFF', _this.config.featureFlags) && _this.socketMessageMetrics) {
142
+ _this.socketMessageMetrics.setupSocketMessageMetrics();
143
+ }
128
144
  _this.connected = true;
129
145
  logger('Connected.', _this.socket.id);
130
146
  var measure = (0, _performance.stopMeasure)(_performance.MEASURE_NAME.SOCKET_CONNECT, _this.analyticsHelper);
@@ -132,7 +148,7 @@ var Channel = /*#__PURE__*/function (_Emitter) {
132
148
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
133
149
  // Potentially incorrect when value of token changes between connecting and connect.
134
150
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
135
- usedCachedToken: _this.token ? true : false
151
+ usedCachedToken: !!_this.token
136
152
  });
137
153
  _this.emit('connected', {
138
154
  sid: _this.socket.id,
@@ -474,6 +490,9 @@ var Channel = /*#__PURE__*/function (_Emitter) {
474
490
  };
475
491
  }
476
492
  this.socket = createSocket("".concat(url, "/session/").concat(documentAri), auth, this.config.productInfo);
493
+ if (this.socket && this.analyticsHelper) {
494
+ this.socketMessageMetrics = new _socketMessageMetrics.SocketMessageMetrics(this.socket, this.analyticsHelper);
495
+ }
477
496
 
478
497
  // Due to https://github.com/socketio/socket.io-client/issues/1473,
479
498
  // reconnect no longer fired on the socket.
@@ -525,6 +544,9 @@ var Channel = /*#__PURE__*/function (_Emitter) {
525
544
  return _regenerator.default.wrap(function _callee3$(_context3) {
526
545
  while (1) switch (_context3.prev = _context3.next) {
527
546
  case 0:
547
+ if ((0, _featureFlags.getCollabProviderFeatureFlag)('socketMessageMetricsFF', _this2.config.featureFlags) && _this2.socketMessageMetrics) {
548
+ _this2.socketMessageMetrics.closeSocketMessageMetrics();
549
+ }
528
550
  _this2.connected = false;
529
551
  logger("disconnect reason: ".concat(reason));
530
552
  _this2.emit('disconnect', {
@@ -546,7 +568,7 @@ var Channel = /*#__PURE__*/function (_Emitter) {
546
568
  _this2.emit('error', reconnectionError);
547
569
  }
548
570
  }
549
- case 4:
571
+ case 5:
550
572
  case "end":
551
573
  return _context3.stop();
552
574
  }
@@ -566,6 +588,12 @@ var Channel = /*#__PURE__*/function (_Emitter) {
566
588
  // Ensure the error emit to the provider has the same structure, so we can handle them unified.
567
589
  this.socket.on('connect_error', this.onConnectError);
568
590
  this.socket.on('permission:invalidateToken', this.handlePermissionInvalidateToken);
591
+ this.socket.onAnyOutgoing(function (event) {
592
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
593
+ args[_key - 1] = arguments[_key];
594
+ }
595
+ return _this2.onAnyOutgoingHandler(Date.now(), args);
596
+ });
569
597
 
570
598
  // To trigger reconnection when browser comes back online
571
599
  if (!this.network) {
@@ -579,6 +607,81 @@ var Channel = /*#__PURE__*/function (_Emitter) {
579
607
  // Fired upon a reconnection attempt error (from Socket.IO Manager)
580
608
  this.socket.io.on('reconnect_error', this.onReconnectError);
581
609
  }
610
+ }, {
611
+ key: "onAnyOutgoingHandler",
612
+ value: function onAnyOutgoingHandler(currentTimeMs, args) {
613
+ var rateLimitType = this.config.rateLimitType || this.RATE_LIMIT_TYPE_NONE;
614
+ if (rateLimitType === this.RATE_LIMIT_TYPE_NONE) {
615
+ return;
616
+ }
617
+ var stepLimit = this.config.rateLimitStepCount || 0;
618
+ var stepSizeLimit = this.config.rateLimitTotalStepSize || 0;
619
+ var maxStepSizeLimit = this.config.rateLimitMaxStepSize || 0;
620
+ var _iterator = _createForOfIteratorHelper(args),
621
+ _step;
622
+ try {
623
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
624
+ var arg = _step.value;
625
+ if (arg.type === 'steps:commit') {
626
+ if (currentTimeMs - this.rateLimitWindowStartMs > this.rateLimitWindowDurationMs) {
627
+ // Start a new window
628
+ this.rateLimitWindowStartMs = currentTimeMs;
629
+ this.stepCounter = 0;
630
+ this.stepSizeCounter = 0;
631
+ this.maxStepSize = 0;
632
+ }
633
+ this.stepCounter += arg.steps.length;
634
+ var _iterator2 = _createForOfIteratorHelper(arg.steps),
635
+ _step2;
636
+ try {
637
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
638
+ var step = _step2.value;
639
+ var stepSize = JSON.stringify(step).length;
640
+ this.stepSizeCounter += stepSize;
641
+ if (stepSize > this.maxStepSize) {
642
+ this.maxStepSize = stepSize;
643
+ }
644
+ }
645
+ } catch (err) {
646
+ _iterator2.e(err);
647
+ } finally {
648
+ _iterator2.f();
649
+ }
650
+ if (this.isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit)) {
651
+ var rateLimitError = {
652
+ message: 'Rate limited',
653
+ data: {
654
+ code: _errorTypes.NCS_ERROR_CODE.RATE_LIMIT_ERROR,
655
+ status: 500,
656
+ meta: {
657
+ rateLimitType: rateLimitType,
658
+ maxStepSize: this.maxStepSize,
659
+ stepSizeCounter: this.stepSizeCounter,
660
+ stepCounter: this.stepCounter
661
+ }
662
+ }
663
+ };
664
+ if (rateLimitType === this.RATE_LIMIT_TYPE_HARD) {
665
+ this.emit('error', rateLimitError);
666
+ throw new Error();
667
+ } else if (rateLimitType === this.RATE_LIMIT_TYPE_SOFT) {
668
+ var _this$analyticsHelper7;
669
+ (_this$analyticsHelper7 = this.analyticsHelper) === null || _this$analyticsHelper7 === void 0 ? void 0 : _this$analyticsHelper7.sendErrorEvent(rateLimitError, 'Rate limited');
670
+ }
671
+ }
672
+ }
673
+ }
674
+ } catch (err) {
675
+ _iterator.e(err);
676
+ } finally {
677
+ _iterator.f();
678
+ }
679
+ }
680
+ }, {
681
+ key: "isLimitExceeded",
682
+ value: function isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit) {
683
+ return stepLimit > 0 && this.stepCounter > stepLimit || stepSizeLimit > 0 && this.stepSizeCounter > stepSizeLimit || maxStepSizeLimit > 0 && this.maxStepSize > maxStepSizeLimit;
684
+ }
582
685
  }, {
583
686
  key: "disconnect",
584
687
  value: function disconnect() {
@@ -103,7 +103,7 @@ var errorCodeMapper = function errorCodeMapper(error) {
103
103
  return {
104
104
  code: _collab.PROVIDER_ERROR_CODE.INTERNAL_SERVICE_ERROR,
105
105
  message: 'Collab Provider experienced an unrecoverable error',
106
- recoverable: false,
106
+ recoverable: true,
107
107
  reason: (_error$data3 = error.data) === null || _error$data3 === void 0 ? void 0 : _error$data3.code,
108
108
  status: 500
109
109
  };
@@ -115,6 +115,12 @@ var errorCodeMapper = function errorCodeMapper(error) {
115
115
  reason: (_error$data4 = error.data) === null || _error$data4 === void 0 ? void 0 : _error$data4.code,
116
116
  status: 500
117
117
  };
118
+ case _errorTypes.NCS_ERROR_CODE.RATE_LIMIT_ERROR:
119
+ return {
120
+ code: _collab.PROVIDER_ERROR_CODE.FAIL_TO_SAVE,
121
+ message: 'Document rate limit',
122
+ recoverable: false
123
+ };
118
124
  default:
119
125
  return;
120
126
  }
@@ -46,6 +46,7 @@ var NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
46
46
  NCS_ERROR_CODE["INVALID_ACTIVATION_ID"] = "INVALID_ACTIVATION_ID";
47
47
  NCS_ERROR_CODE["INVALID_DOCUMENT_ARI"] = "INVALID_DOCUMENT_ARI";
48
48
  NCS_ERROR_CODE["INVALID_CLOUD_ID"] = "INVALID_CLOUD_ID";
49
+ NCS_ERROR_CODE["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
49
50
  return NCS_ERROR_CODE;
50
51
  }({}); // TODO: Import emitted error codes from NCS
51
52
  // NCS Errors
@@ -60,6 +61,10 @@ var NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
60
61
  * When we try to apply state updates to the editor, if that fails to apply the user can enter an invalid state where no
61
62
  * changes can be saved to NCS.
62
63
  */
64
+ /**
65
+ * The client is trying to send too many messages or messages that are too large. This not expected to be a standard
66
+ * operating condition and should only ever indicate a frontend bug.
67
+ */
63
68
  /**
64
69
  * A union of all possible internal errors, that are mapped to another error if being emitted to the editor.
65
70
  */
@@ -4,9 +4,10 @@ var _index = require("../index");
4
4
  describe('Feature flags', function () {
5
5
  it('getProductSpecificFeatureFlags', function () {
6
6
  var result = (0, _index.getProductSpecificFeatureFlags)({
7
- testFF: true
7
+ testFF: true,
8
+ socketMessageMetricsFF: true
8
9
  }, 'confluence');
9
- expect(result).toEqual(['confluence.fe.collab.provider.testFF']);
10
+ expect(result).toEqual(['confluence.fe.collab.provider.testFF', 'confluence.fe.collab.provider.socketMessageMetricsFF']);
10
11
  });
11
12
  it('getCollabProviderFeatureFlag return true', function () {
12
13
  var result = (0, _index.getCollabProviderFeatureFlag)('testFF', {
@@ -8,7 +8,8 @@ exports.getCollabProviderFeatureFlag = getCollabProviderFeatureFlag;
8
8
  exports.getProductSpecificFeatureFlags = void 0;
9
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
10
  var defaultNCSFeatureFlags = {
11
- testFF: false
11
+ testFF: false,
12
+ socketMessageMetricsFF: false
12
13
  };
13
14
 
14
15
  /**
@@ -16,7 +17,8 @@ var defaultNCSFeatureFlags = {
16
17
  */
17
18
  var productKeys = {
18
19
  confluence: {
19
- testFF: 'confluence.fe.collab.provider.testFF'
20
+ testFF: 'confluence.fe.collab.provider.testFF',
21
+ socketMessageMetricsFF: 'confluence.fe.collab.provider.socketMessageMetricsFF'
20
22
  }
21
23
  };
22
24
  var filterFeatureFlagNames = function filterFeatureFlagNames(flags) {
@@ -19,8 +19,9 @@ var EVENT_ACTION = /*#__PURE__*/function (EVENT_ACTION) {
19
19
  EVENT_ACTION["SEND_STEPS_RETRY"] = "sendStepsRetry";
20
20
  EVENT_ACTION["CATCHUP_AFTER_MAX_SEND_STEPS_RETRY"] = "catchupAfterMaxSendStepsRetry";
21
21
  EVENT_ACTION["DROPPED_STEPS"] = "droppedStepInCatchup";
22
+ EVENT_ACTION["WEBSOCKET_MESSAGE_VOLUME_METRIC"] = "websocketMessageVolumeMetric";
22
23
  return EVENT_ACTION;
23
- }({}); // https://data-portal.internal.atlassian.com/analytics/registry/53724
24
+ }({}); // https://data-portal.internal.atlassian.com/analytics/registry/53596
24
25
  exports.EVENT_ACTION = EVENT_ACTION;
25
26
  var EVENT_STATUS = /*#__PURE__*/function (EVENT_STATUS) {
26
27
  EVENT_STATUS["SUCCESS"] = "SUCCESS";
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS = exports.SocketMessageMetrics = void 0;
8
+ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
9
+ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
10
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+ var _const = require("./const");
12
+ var _utils = require("./utils");
13
+ var logger = (0, _utils.createLogger)('SocketMessageMetrics', 'green');
14
+ var WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS = 60000;
15
+ exports.WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS = WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS;
16
+ var SocketMessageMetrics = /*#__PURE__*/(0, _createClass2.default)(function SocketMessageMetrics(socket, analyticsHelper) {
17
+ var _this = this;
18
+ (0, _classCallCheck2.default)(this, SocketMessageMetrics);
19
+ (0, _defineProperty2.default)(this, "messageCount", 0);
20
+ (0, _defineProperty2.default)(this, "totalMessageSize", 0);
21
+ (0, _defineProperty2.default)(this, "metricsIntervalID", undefined);
22
+ (0, _defineProperty2.default)(this, "socketMessageMetricsListener", function (event) {
23
+ _this.messageCount++;
24
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
25
+ args[_key - 1] = arguments[_key];
26
+ }
27
+ _this.totalMessageSize += Buffer.byteLength(JSON.stringify(args), 'utf8');
28
+ });
29
+ (0, _defineProperty2.default)(this, "setupSocketMessageMetrics", function () {
30
+ if (_this.metricsIntervalID !== undefined) {
31
+ logger('calling setupSocketMessageMetrics function with metricsIntervalID that is not undefined');
32
+ return;
33
+ }
34
+ _this.socket.onAnyOutgoing(_this.socketMessageMetricsListener);
35
+
36
+ // send metrics every 60 seconds
37
+ _this.metricsIntervalID = window.setInterval(function () {
38
+ _this.analyticsHelper.sendActionEvent(_const.EVENT_ACTION.WEBSOCKET_MESSAGE_VOLUME_METRIC, _const.EVENT_STATUS.INFO, {
39
+ messageCount: _this.messageCount,
40
+ totalMessageSize: _this.totalMessageSize
41
+ });
42
+ _this.messageCount = 0;
43
+ _this.totalMessageSize = 0;
44
+ }, WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS);
45
+ });
46
+ (0, _defineProperty2.default)(this, "closeSocketMessageMetrics", function () {
47
+ clearInterval(_this.metricsIntervalID);
48
+ _this.metricsIntervalID = undefined;
49
+ _this.socket.offAnyOutgoing(_this.socketMessageMetricsListener);
50
+ });
51
+ this.socket = socket;
52
+ this.analyticsHelper = analyticsHelper;
53
+ });
54
+ exports.SocketMessageMetrics = SocketMessageMetrics;
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.version = exports.nextMajorVersion = exports.name = void 0;
7
7
  var name = "@atlaskit/collab-provider";
8
8
  exports.name = name;
9
- var version = "9.7.1";
9
+ var version = "9.7.3";
10
10
  exports.version = version;
11
11
  var nextMajorVersion = function nextMajorVersion() {
12
12
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "9.7.1",
3
+ "version": "9.7.3",
4
4
  "sideEffects": false
5
5
  }
@@ -8,16 +8,26 @@ import ReconnectHelper from './connectivity/reconnect-helper';
8
8
  import { createDocInitExp } from './analytics/ufo';
9
9
  import { socketIOReasons } from './disconnected-reason-mapper';
10
10
  import Network from './connectivity/network';
11
- import { NotConnectedError, NotInitializedError, INTERNAL_ERROR_CODE } from './errors/error-types';
11
+ import { NotConnectedError, NotInitializedError, INTERNAL_ERROR_CODE, NCS_ERROR_CODE } from './errors/error-types';
12
+ import { SocketMessageMetrics } from './helpers/socket-message-metrics';
13
+ import { getCollabProviderFeatureFlag } from './feature-flags';
12
14
  const logger = createLogger('Channel', 'green');
13
15
  export class Channel extends Emitter {
14
16
  constructor(config, analyticsHelper) {
15
17
  super();
18
+ _defineProperty(this, "RATE_LIMIT_TYPE_NONE", 0);
19
+ _defineProperty(this, "RATE_LIMIT_TYPE_SOFT", 1);
20
+ _defineProperty(this, "RATE_LIMIT_TYPE_HARD", 2);
16
21
  _defineProperty(this, "connected", false);
17
22
  _defineProperty(this, "socket", null);
18
23
  _defineProperty(this, "reconnectHelper", null);
19
24
  _defineProperty(this, "initialized", false);
20
25
  _defineProperty(this, "network", null);
26
+ _defineProperty(this, "rateLimitWindowDurationMs", 60000);
27
+ _defineProperty(this, "rateLimitWindowStartMs", 0);
28
+ _defineProperty(this, "stepCounter", 0);
29
+ _defineProperty(this, "stepSizeCounter", 0);
30
+ _defineProperty(this, "maxStepSize", 0);
21
31
  // read-only getters used for tests
22
32
  _defineProperty(this, "getInitialized", () => this.initialized);
23
33
  _defineProperty(this, "getConnected", () => this.connected);
@@ -37,7 +47,7 @@ export class Channel extends Emitter {
37
47
  reason: data.reason,
38
48
  // Potentially incorrect when value of token changes between connecting and connect.
39
49
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
40
- usedCachedToken: this.token ? true : false
50
+ usedCachedToken: !!this.token
41
51
  });
42
52
  this.unsetToken();
43
53
  });
@@ -48,7 +58,7 @@ export class Channel extends Emitter {
48
58
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
49
59
  // Potentially incorrect when value of token changes between connecting and connect.
50
60
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
51
- usedCachedToken: this.token ? true : false
61
+ usedCachedToken: !!this.token
52
62
  });
53
63
  (_this$analyticsHelper3 = this.analyticsHelper) === null || _this$analyticsHelper3 === void 0 ? void 0 : _this$analyticsHelper3.sendErrorEvent(error, 'Error while establishing connection');
54
64
  // If error received with `data`, it means the connection is rejected
@@ -93,6 +103,9 @@ export class Channel extends Emitter {
93
103
  });
94
104
  _defineProperty(this, "onConnect", () => {
95
105
  var _this$analyticsHelper5;
106
+ if (getCollabProviderFeatureFlag('socketMessageMetricsFF', this.config.featureFlags) && this.socketMessageMetrics) {
107
+ this.socketMessageMetrics.setupSocketMessageMetrics();
108
+ }
96
109
  this.connected = true;
97
110
  logger('Connected.', this.socket.id);
98
111
  const measure = stopMeasure(MEASURE_NAME.SOCKET_CONNECT, this.analyticsHelper);
@@ -100,7 +113,7 @@ export class Channel extends Emitter {
100
113
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
101
114
  // Potentially incorrect when value of token changes between connecting and connect.
102
115
  // See: https://bitbucket.org/atlassian/%7Bc8e2f021-38d2-46d0-9b7a-b3f7b428f724%7D/pull-requests/29905#comment-375308874
103
- usedCachedToken: this.token ? true : false
116
+ usedCachedToken: !!this.token
104
117
  });
105
118
  this.emit('connected', {
106
119
  sid: this.socket.id,
@@ -350,6 +363,9 @@ export class Channel extends Emitter {
350
363
  };
351
364
  }
352
365
  this.socket = createSocket(`${url}/session/${documentAri}`, auth, this.config.productInfo);
366
+ if (this.socket && this.analyticsHelper) {
367
+ this.socketMessageMetrics = new SocketMessageMetrics(this.socket, this.analyticsHelper);
368
+ }
353
369
 
354
370
  // Due to https://github.com/socketio/socket.io-client/issues/1473,
355
371
  // reconnect no longer fired on the socket.
@@ -400,6 +416,9 @@ export class Channel extends Emitter {
400
416
  this.emit('status', data);
401
417
  });
402
418
  this.socket.on('disconnect', async reason => {
419
+ if (getCollabProviderFeatureFlag('socketMessageMetricsFF', this.config.featureFlags) && this.socketMessageMetrics) {
420
+ this.socketMessageMetrics.closeSocketMessageMetrics();
421
+ }
403
422
  this.connected = false;
404
423
  logger(`disconnect reason: ${reason}`);
405
424
  this.emit('disconnect', {
@@ -433,6 +452,7 @@ export class Channel extends Emitter {
433
452
  // Ensure the error emit to the provider has the same structure, so we can handle them unified.
434
453
  this.socket.on('connect_error', this.onConnectError);
435
454
  this.socket.on('permission:invalidateToken', this.handlePermissionInvalidateToken);
455
+ this.socket.onAnyOutgoing((event, ...args) => this.onAnyOutgoingHandler(Date.now(), args));
436
456
 
437
457
  // To trigger reconnection when browser comes back online
438
458
  if (!this.network) {
@@ -446,6 +466,59 @@ export class Channel extends Emitter {
446
466
  // Fired upon a reconnection attempt error (from Socket.IO Manager)
447
467
  this.socket.io.on('reconnect_error', this.onReconnectError);
448
468
  }
469
+ onAnyOutgoingHandler(currentTimeMs, args) {
470
+ const rateLimitType = this.config.rateLimitType || this.RATE_LIMIT_TYPE_NONE;
471
+ if (rateLimitType === this.RATE_LIMIT_TYPE_NONE) {
472
+ return;
473
+ }
474
+ const stepLimit = this.config.rateLimitStepCount || 0;
475
+ const stepSizeLimit = this.config.rateLimitTotalStepSize || 0;
476
+ const maxStepSizeLimit = this.config.rateLimitMaxStepSize || 0;
477
+ for (const arg of args) {
478
+ if (arg.type === 'steps:commit') {
479
+ if (currentTimeMs - this.rateLimitWindowStartMs > this.rateLimitWindowDurationMs) {
480
+ // Start a new window
481
+ this.rateLimitWindowStartMs = currentTimeMs;
482
+ this.stepCounter = 0;
483
+ this.stepSizeCounter = 0;
484
+ this.maxStepSize = 0;
485
+ }
486
+ this.stepCounter += arg.steps.length;
487
+ for (const step of arg.steps) {
488
+ const stepSize = JSON.stringify(step).length;
489
+ this.stepSizeCounter += stepSize;
490
+ if (stepSize > this.maxStepSize) {
491
+ this.maxStepSize = stepSize;
492
+ }
493
+ }
494
+ if (this.isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit)) {
495
+ const rateLimitError = {
496
+ message: 'Rate limited',
497
+ data: {
498
+ code: NCS_ERROR_CODE.RATE_LIMIT_ERROR,
499
+ status: 500,
500
+ meta: {
501
+ rateLimitType,
502
+ maxStepSize: this.maxStepSize,
503
+ stepSizeCounter: this.stepSizeCounter,
504
+ stepCounter: this.stepCounter
505
+ }
506
+ }
507
+ };
508
+ if (rateLimitType === this.RATE_LIMIT_TYPE_HARD) {
509
+ this.emit('error', rateLimitError);
510
+ throw new Error();
511
+ } else if (rateLimitType === this.RATE_LIMIT_TYPE_SOFT) {
512
+ var _this$analyticsHelper8;
513
+ (_this$analyticsHelper8 = this.analyticsHelper) === null || _this$analyticsHelper8 === void 0 ? void 0 : _this$analyticsHelper8.sendErrorEvent(rateLimitError, 'Rate limited');
514
+ }
515
+ }
516
+ }
517
+ }
518
+ }
519
+ isLimitExceeded(stepLimit, stepSizeLimit, maxStepSizeLimit) {
520
+ return stepLimit > 0 && this.stepCounter > stepLimit || stepSizeLimit > 0 && this.stepSizeCounter > stepSizeLimit || maxStepSizeLimit > 0 && this.maxStepSize > maxStepSizeLimit;
521
+ }
449
522
  disconnect() {
450
523
  var _this$network;
451
524
  this.unsubscribeAll();
@@ -99,7 +99,7 @@ export const errorCodeMapper = error => {
99
99
  return {
100
100
  code: PROVIDER_ERROR_CODE.INTERNAL_SERVICE_ERROR,
101
101
  message: 'Collab Provider experienced an unrecoverable error',
102
- recoverable: false,
102
+ recoverable: true,
103
103
  reason: (_error$data3 = error.data) === null || _error$data3 === void 0 ? void 0 : _error$data3.code,
104
104
  status: 500
105
105
  };
@@ -111,6 +111,12 @@ export const errorCodeMapper = error => {
111
111
  reason: (_error$data4 = error.data) === null || _error$data4 === void 0 ? void 0 : _error$data4.code,
112
112
  status: 500
113
113
  };
114
+ case NCS_ERROR_CODE.RATE_LIMIT_ERROR:
115
+ return {
116
+ code: PROVIDER_ERROR_CODE.FAIL_TO_SAVE,
117
+ message: 'Document rate limit',
118
+ recoverable: false
119
+ };
114
120
  default:
115
121
  return;
116
122
  }
@@ -32,6 +32,7 @@ export let NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
32
32
  NCS_ERROR_CODE["INVALID_ACTIVATION_ID"] = "INVALID_ACTIVATION_ID";
33
33
  NCS_ERROR_CODE["INVALID_DOCUMENT_ARI"] = "INVALID_DOCUMENT_ARI";
34
34
  NCS_ERROR_CODE["INVALID_CLOUD_ID"] = "INVALID_CLOUD_ID";
35
+ NCS_ERROR_CODE["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
35
36
  return NCS_ERROR_CODE;
36
37
  }({});
37
38
 
@@ -49,6 +50,10 @@ export let NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
49
50
  * When we try to apply state updates to the editor, if that fails to apply the user can enter an invalid state where no
50
51
  * changes can be saved to NCS.
51
52
  */
53
+ /**
54
+ * The client is trying to send too many messages or messages that are too large. This not expected to be a standard
55
+ * operating condition and should only ever indicate a frontend bug.
56
+ */
52
57
  /**
53
58
  * A union of all possible internal errors, that are mapped to another error if being emitted to the editor.
54
59
  */
@@ -2,9 +2,10 @@ import { getProductSpecificFeatureFlags, getCollabProviderFeatureFlag } from '..
2
2
  describe('Feature flags', () => {
3
3
  it('getProductSpecificFeatureFlags', () => {
4
4
  const result = getProductSpecificFeatureFlags({
5
- testFF: true
5
+ testFF: true,
6
+ socketMessageMetricsFF: true
6
7
  }, 'confluence');
7
- expect(result).toEqual(['confluence.fe.collab.provider.testFF']);
8
+ expect(result).toEqual(['confluence.fe.collab.provider.testFF', 'confluence.fe.collab.provider.socketMessageMetricsFF']);
8
9
  });
9
10
  it('getCollabProviderFeatureFlag return true', () => {
10
11
  const result = getCollabProviderFeatureFlag('testFF', {
@@ -1,5 +1,6 @@
1
1
  const defaultNCSFeatureFlags = {
2
- testFF: false
2
+ testFF: false,
3
+ socketMessageMetricsFF: false
3
4
  };
4
5
 
5
6
  /**
@@ -7,7 +8,8 @@ const defaultNCSFeatureFlags = {
7
8
  */
8
9
  const productKeys = {
9
10
  confluence: {
10
- testFF: 'confluence.fe.collab.provider.testFF'
11
+ testFF: 'confluence.fe.collab.provider.testFF',
12
+ socketMessageMetricsFF: 'confluence.fe.collab.provider.socketMessageMetricsFF'
11
13
  }
12
14
  };
13
15
  const filterFeatureFlagNames = flags => {
@@ -13,8 +13,9 @@ export let EVENT_ACTION = /*#__PURE__*/function (EVENT_ACTION) {
13
13
  EVENT_ACTION["SEND_STEPS_RETRY"] = "sendStepsRetry";
14
14
  EVENT_ACTION["CATCHUP_AFTER_MAX_SEND_STEPS_RETRY"] = "catchupAfterMaxSendStepsRetry";
15
15
  EVENT_ACTION["DROPPED_STEPS"] = "droppedStepInCatchup";
16
+ EVENT_ACTION["WEBSOCKET_MESSAGE_VOLUME_METRIC"] = "websocketMessageVolumeMetric";
16
17
  return EVENT_ACTION;
17
- }({}); // https://data-portal.internal.atlassian.com/analytics/registry/53724
18
+ }({}); // https://data-portal.internal.atlassian.com/analytics/registry/53596
18
19
  export let EVENT_STATUS = /*#__PURE__*/function (EVENT_STATUS) {
19
20
  EVENT_STATUS["SUCCESS"] = "SUCCESS";
20
21
  EVENT_STATUS["FAILURE"] = "FAILURE";
@@ -0,0 +1,40 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { EVENT_ACTION, EVENT_STATUS } from './const';
3
+ import { createLogger } from './utils';
4
+ const logger = createLogger('SocketMessageMetrics', 'green');
5
+ export const WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS = 60000;
6
+ export class SocketMessageMetrics {
7
+ constructor(socket, analyticsHelper) {
8
+ _defineProperty(this, "messageCount", 0);
9
+ _defineProperty(this, "totalMessageSize", 0);
10
+ _defineProperty(this, "metricsIntervalID", undefined);
11
+ _defineProperty(this, "socketMessageMetricsListener", (event, ...args) => {
12
+ this.messageCount++;
13
+ this.totalMessageSize += Buffer.byteLength(JSON.stringify(args), 'utf8');
14
+ });
15
+ _defineProperty(this, "setupSocketMessageMetrics", () => {
16
+ if (this.metricsIntervalID !== undefined) {
17
+ logger('calling setupSocketMessageMetrics function with metricsIntervalID that is not undefined');
18
+ return;
19
+ }
20
+ this.socket.onAnyOutgoing(this.socketMessageMetricsListener);
21
+
22
+ // send metrics every 60 seconds
23
+ this.metricsIntervalID = window.setInterval(() => {
24
+ this.analyticsHelper.sendActionEvent(EVENT_ACTION.WEBSOCKET_MESSAGE_VOLUME_METRIC, EVENT_STATUS.INFO, {
25
+ messageCount: this.messageCount,
26
+ totalMessageSize: this.totalMessageSize
27
+ });
28
+ this.messageCount = 0;
29
+ this.totalMessageSize = 0;
30
+ }, WEBSOCKET_MESSAGE_VOLUME_METRIC_SEND_INTERVAL_MS);
31
+ });
32
+ _defineProperty(this, "closeSocketMessageMetrics", () => {
33
+ clearInterval(this.metricsIntervalID);
34
+ this.metricsIntervalID = undefined;
35
+ this.socket.offAnyOutgoing(this.socketMessageMetricsListener);
36
+ });
37
+ this.socket = socket;
38
+ this.analyticsHelper = analyticsHelper;
39
+ }
40
+ }