Просмотр исходного кода

Merge pull request #18225 from overleaf/em-typescript-eslint

Add typescript-eslint rule: no-floating-promises

GitOrigin-RevId: 8c3decdff537c885f5bfeb5250b7805480bc6602
Eric Mc Sween 2 лет назад
Родитель
Сommit
876ee4d967
48 измененных файлов с 1141 добавлено и 783 удалено
  1. 348 223
      package-lock.json
  2. 0 370
      services/web/.eslintrc
  3. 432 0
      services/web/.eslintrc.js
  4. 31 0
      services/web/app/src/Features/Analytics/AnalyticsManager.js
  5. 7 7
      services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.js
  6. 1 1
      services/web/app/src/Features/Analytics/AnalyticsUTMTrackingMiddleware.js
  7. 1 1
      services/web/app/src/Features/Authentication/AuthenticationController.js
  8. 10 2
      services/web/app/src/Features/BetaProgram/BetaProgramHandler.js
  9. 1 1
      services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js
  10. 1 1
      services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js
  11. 1 1
      services/web/app/src/Features/Collaborators/OwnershipTransferHandler.js
  12. 4 1
      services/web/app/src/Features/Email/EmailHandler.js
  13. 7 3
      services/web/app/src/Features/LinkedFiles/LinkedFilesController.js
  14. 11 3
      services/web/app/src/Features/Project/ProjectController.js
  15. 5 5
      services/web/app/src/Features/Project/ProjectCreationHandler.js
  16. 4 0
      services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js
  17. 27 3
      services/web/app/src/Features/SplitTests/SplitTestHandler.js
  18. 1 1
      services/web/app/src/Features/Subscription/FeaturesUpdater.js
  19. 102 54
      services/web/app/src/Features/Subscription/RecurlyEventHandler.js
  20. 3 3
      services/web/app/src/Features/Subscription/SubscriptionUpdater.js
  21. 9 3
      services/web/app/src/Features/SystemMessages/SystemMessageManager.js
  22. 2 2
      services/web/app/src/Features/TokenAccess/TokenAccessHandler.js
  23. 9 9
      services/web/app/src/Features/User/ThirdPartyIdentityManager.js
  24. 5 1
      services/web/app/src/Features/User/UserCreator.js
  25. 15 8
      services/web/app/src/Features/User/UserEmailsController.js
  26. 5 5
      services/web/app/src/Features/User/UserRegistrationHandler.js
  27. 8 2
      services/web/app/src/Features/User/UserUpdater.js
  28. 3 0
      services/web/app/src/infrastructure/Translations.js
  29. 4 0
      services/web/modules/launchpad/app/src/LaunchpadController.js
  30. 2 2
      services/web/package.json
  31. 1 1
      services/web/scripts/refresh_features.js
  32. 7 5
      services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddlewareTests.js
  33. 2 2
      services/web/test/unit/src/Authentication/AuthenticationControllerTests.js
  34. 3 3
      services/web/test/unit/src/BetaProgram/BetaProgramHandlerTests.js
  35. 1 1
      services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js
  36. 3 2
      services/web/test/unit/src/Collaborators/OwnershipTransferHandlerTests.js
  37. 1 1
      services/web/test/unit/src/Email/EmailHandlerTests.js
  38. 3 1
      services/web/test/unit/src/Project/ProjectControllerTests.js
  39. 1 1
      services/web/test/unit/src/SplitTests/SplitTestHandlerTests.js
  40. 3 3
      services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js
  41. 36 30
      services/web/test/unit/src/Subscription/RecurlyEventHandlerTests.js
  42. 11 11
      services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js
  43. 3 3
      services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js
  44. 2 2
      services/web/test/unit/src/User/ThirdPartyIdentityManagerTests.js
  45. 2 2
      services/web/test/unit/src/User/UserCreatorTests.js
  46. 1 1
      services/web/test/unit/src/User/UserEmailsControllerTests.js
  47. 1 1
      services/web/test/unit/src/User/UserRegistrationHandlerTests.js
  48. 1 1
      services/web/test/unit/src/User/UserUpdaterTests.js

Разница между файлами не показана из-за своего большого размера
+ 348 - 223
package-lock.json


+ 0 - 370
services/web/.eslintrc

@@ -1,370 +0,0 @@
-{
-  "root": true,
-  "parser": "@typescript-eslint/parser",
-  "extends": [
-    "eslint:recommended",
-    "plugin:@typescript-eslint/recommended",
-    "standard",
-    "prettier"
-  ],
-  "plugins": ["@overleaf"],
-  "env": {
-    "es2020": true
-  },
-  "settings": {
-    // Tell eslint-plugin-react to detect which version of React we are using
-    "react": {
-      "version": "detect"
-    }
-  },
-  "rules": {
-    "no-constant-binary-expression": "error",
-
-    // do not allow importing of implicit dependencies.
-    "import/no-extraneous-dependencies": "error",
-
-    // disable some TypeScript rules
-    "@typescript-eslint/no-var-requires": "off",
-    "@typescript-eslint/no-unused-vars": "off",
-    "@typescript-eslint/no-empty-function": "off",
-    "@typescript-eslint/no-explicit-any": "off",
-    "@typescript-eslint/no-this-alias": "off",
-    "@typescript-eslint/no-non-null-assertion": "off",
-    "@typescript-eslint/ban-ts-comment": "off"
-  },
-  "overrides": [
-    // NOTE: changing paths may require updating them in the Makefile too.
-    {
-      // Node
-      "files": [
-        "**/app/src/**/*.js",
-        "app.js",
-        "i18next-scanner.config.js",
-        "scripts/**/*.js",
-        "webpack.config*.js"
-      ],
-      "env": {
-        "node": true
-      }
-    },
-    {
-      // Test specific rules
-      "files": ["**/test/**/*.*"],
-      "plugins": [
-        "mocha",
-        "chai-expect",
-        "chai-friendly"
-      ],
-      "env": {
-        "mocha": true
-      },
-      "rules": {
-        // mocha-specific rules
-        "mocha/handle-done-callback": "error",
-        "mocha/no-exclusive-tests": "error",
-        "mocha/no-global-tests": "error",
-        "mocha/no-identical-title": "error",
-        "mocha/no-nested-tests": "error",
-        "mocha/no-pending-tests": "error",
-        "mocha/no-skipped-tests": "error",
-        "mocha/no-mocha-arrows": "error",
-
-
-        // Swap the no-unused-expressions rule with a more chai-friendly one
-        "no-unused-expressions": "off",
-        "chai-friendly/no-unused-expressions": "error",
-
-        // chai-specific rules
-        "chai-expect/missing-assertion": "error",
-        "chai-expect/terminating-properties": "error",
-
-        // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
-        // we don't enforce this at the top-level - just in tests to manage `this` scope
-        // based on mocha's context mechanism
-        "mocha/prefer-arrow-callback": "error"
-      }
-    },
-    {
-      // Backend specific rules
-      "files": ["**/app/src/**/*.js", "app.js"],
-      "rules": {
-        // do not allow importing of implicit dependencies.
-        "import/no-extraneous-dependencies": ["error", {
-          // do not allow importing of devDependencies.
-          "devDependencies": false
-        }],
-        "no-restricted-syntax": [
-          "error",
-          // do not allow node-fetch in backend code
-          {
-            "selector": "CallExpression[callee.name='require'] > .arguments[value='node-fetch']",
-            "message": "Requiring node-fetch is not allowed in production services, please use fetch-utils."
-          },
-          // mongoose populate must set fields to populate
-          {
-            "selector": "CallExpression[callee.property.name='populate'][arguments.length<2]",
-            "message": "Populate without a second argument returns the whole document. Use populate('field',['prop1','prop2']) instead"
-          },
-          // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
-          {
-            "selector": "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
-            "message": "Construct ObjectId with `new ObjectId()` instead of `ObjectId()`"
-          },
-          // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
-          {
-            "selector": "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
-            "message": "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead"
-          }
-        ]
-      }
-    },
-    {
-      // Backend tests and scripts specific rules
-      "files": ["**/test/**/*.*", "**/scripts/*.*"],
-      "rules": {
-        "no-restricted-syntax": [
-          "error",
-          // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
-          {
-            "selector": "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
-            "message": "Construct ObjectId with `new ObjectId()` instead of `ObjectId()`"
-          },
-          // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
-          {
-            "selector": "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
-            "message": "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead"
-          },
-          // Catch incorrect usage of `await db.collection.find()`
-          {
-            "selector": "AwaitExpression > CallExpression > MemberExpression[property.name='find'][object.object.name='db']",
-            "message": "Mongo find returns a cursor not a promise, use `for await (const result of cursor)` or `.toArray()` instead."
-          }
-        ]
-      }
-    },
-    {
-      // Cypress specific rules
-      "files": ["cypress/**/*.{js,jsx,ts,tsx}", "**/test/frontend/**/*.spec.{js,jsx,ts,tsx}"],
-      "extends": [
-        "plugin:cypress/recommended"
-      ]
-    },
-    {
-      // Frontend specific rules
-      "files": ["**/frontend/js/**/*.{js,jsx,ts,tsx}", "**/frontend/stories/**/*.{js,jsx,ts,tsx}", "**/*.stories.{js,jsx,ts,tsx}", "**/test/frontend/**/*.{js,jsx,ts,tsx}", "**/test/frontend/components/**/*.spec.{js,jsx,ts,tsx}"],
-      "env": {
-        "browser": true
-      },
-      "parserOptions": {
-        "sourceType": "module"
-      },
-      "plugins": [
-        "jsx-a11y"
-      ],
-      "extends": [
-        "plugin:react/recommended",
-        "plugin:react-hooks/recommended",
-        "plugin:jsx-a11y/recommended",
-        "standard-jsx",
-        "prettier"
-      ],
-      "globals": {
-        "__webpack_public_path__": true,
-        "$": true,
-        "angular": true,
-        "ga": true,
-        // Injected in layout.pug
-        "user_id": true,
-        "ExposedSettings": true
-      },
-      "rules": {
-        // TODO: remove once https://github.com/standard/eslint-config-standard-react/issues/68 (support eslint@8) is fixed.
-        // START: inline standard-react rules
-        // "react/jsx-no-bind": ["error", {
-        //   "allowArrowFunctions": true,
-        //   "allowBind": false,
-        //   "ignoreRefs": true
-        // },],
-        "react/no-did-update-set-state": "error",
-        "react/no-unused-prop-types": "error",
-        "react/prop-types": "error",
-        // "react/react-in-jsx-scope": "error",
-        // END: inline standard-react rules
-
-        "react/no-unknown-property": ["error", {
-          "ignore": ["dnd-container", "dropdown-toggle"]
-        }],
-
-        "react/jsx-no-target-blank": ["error", {
-          "allowReferrer": true
-        }],
-        // Prevent usage of legacy string refs
-        "react/no-string-refs": "error",
-
-        // Prevent curly braces around strings (as they're unnecessary)
-        "react/jsx-curly-brace-presence": ["error", {
-          "props": "never",
-          "children": "never"
-        }],
-
-        // Don't import React for JSX; the JSX runtime is added by a Babel plugin
-        "react/react-in-jsx-scope": "off",
-        "react/jsx-uses-react": "off",
-
-        // Allow functions as JSX props
-        "react/jsx-no-bind": "off", // TODO: fix occurrences and re-enable this
-
-        // Fix conflict between prettier & standard by overriding to prefer
-        // double quotes
-        "jsx-quotes": ["error", "prefer-double"],
-
-        // Override weird behaviour of jsx-a11y label-has-for (says labels must be
-        // nested *and* have for/id attributes)
-        "jsx-a11y/label-has-for": [
-          "error",
-          {
-            "required": {
-              "some": [
-                "nesting",
-                "id"
-              ]
-            }
-          }
-        ],
-
-        // Require .jsx or .tsx file extension when using JSX
-        "react/jsx-filename-extension": ["error", {
-          "extensions": [".jsx", ".tsx"]
-        }],
-        "no-restricted-syntax": [
-          "error",
-          // Begin: Make sure angular can withstand minification
-          {
-            "selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > :function[params.length > 0]",
-            "message": "Wrap the function in an array with the parameter names, to withstand minifcation. E.g. App.controller('MyController', ['param1', function(param1) {}]"
-          },
-          {
-            "selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > ArrowFunctionExpression",
-            "message": "Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}"
-          },
-          {
-            "selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrowFunctionExpression",
-            "message": "Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}"
-          },
-          {
-            "selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > :not(:function, Identifier):last-child",
-            "message": "Last element of the array must be a function. E.g ['param1', function(param1) {}]"
-          },
-          {
-            "selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression[elements.length=0]",
-            "message": "Array must not be empty. Add parameters and a function. E.g ['param1', function(param1) {}]"
-          },
-          // End: Make sure angular can withstand minification
-          // prohibit direct calls to methods of window.localStorage
-          {
-            "selector": "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
-            "message": "Modify location via customLocalStorage instead of calling window.localStorage methods directly"
-          }
-        ]
-      }
-    },
-    {
-      // React component specific rules
-      //
-      "files": ["**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}", "**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}"],
-      "rules": {
-        "@overleaf/no-unnecessary-trans": "error",
-        "@overleaf/should-unescape-trans": "error",
-
-        // https://astexplorer.net/
-        "no-restricted-syntax": [
-          "error",
-          // prohibit direct calls to methods of window.location
-          {
-            "selector": "CallExpression[callee.object.object.name='window'][callee.object.property.name='location']",
-            "message": "Modify location via useLocation instead of calling window.location methods directly"
-          },
-          // prohibit assignment to window.location
-          {
-            "selector": "AssignmentExpression[left.object.name='window'][left.property.name='location']",
-            "message": "Modify location via useLocation instead of calling window.location methods directly"
-          },
-          // prohibit assignment to window.location.href
-          {
-            "selector": "AssignmentExpression[left.object.object.name='window'][left.object.property.name='location'][left.property.name='href']",
-            "message": "Modify location via useLocation instead of calling window.location methods directly"
-          },
-          // prohibit using lookbehinds due to incidents with Safari simply crashing when the script is parsed
-          {
-            "selector": "Literal[regex.pattern=/\\(\\?<[!=]/]",
-            "message": "Lookbehind is not supported in older Safari versions."
-          },
-          // prohibit direct calls to methods of window.localStorage
-          // NOTE: this rule is also defined for all frontend files, but those rules are overriden by the React component-specific config
-          {
-            "selector": "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
-            "message": "Modify location via customLocalStorage instead of calling window.localStorage methods directly"
-          }
-        ]
-      }
-    },
-    // React + TypeScript-specific rules
-    {
-      "files": ["**/*.tsx"],
-      "rules": {
-        "react/prop-types": "off",
-        "no-undef": "off"
-      }
-    },
-    // TypeScript-specific rules
-    {
-      "files": ["**/*.ts"],
-      "rules": {
-        "no-undef": "off"
-      }
-    },
-    {
-      "files": ["scripts/ukamf/*.js"],
-      "rules": {
-        // Do not allow importing of any dependencies unless specified in either
-        //  - web/package.json
-        //  - web/scripts/ukamf/package.json
-        "import/no-extraneous-dependencies": ["error", {"packageDir": [".", "scripts/ukamf"]}]
-      }
-    },
-    {
-      "files": ["scripts/learn/checkSanitize/*.js"],
-      "rules": {
-        // The checkSanitize script is used in the dev-env only.
-        "import/no-extraneous-dependencies": ["error", {
-          "devDependencies": true,
-          "packageDir": [".", "../../"]
-        }]
-      }
-    },
-    {
-      "files": [
-        // Backend: Use @overleaf/logger
-        //          Docs: https://manual.dev-overleaf.com/development/code/logging/#structured-logging
-        "**/app/**/*.{js,cjs,mjs}", "app.js", "modules/*/*.js",
-        // Frontend: Prefer debugConsole over bare console
-        //           Docs: https://manual.dev-overleaf.com/development/code/logging/#frontend
-        "**/frontend/**/*.{js,jsx,ts,tsx}",
-	// Tests
-	"**/test/**/*.{js,cjs,mjs,jsx,ts,tsx}"
-      ],
-      "excludedFiles": [
-        // Allow console logs in scripts
-        "**/scripts/**/*.js",
-        // Allow console logs in stories
-        "**/stories/**/*.{js,jsx,ts,tsx}",
-        // Workers do not have access to the search params for enabling ?debug=true.
-        // self.location.url is the URL of the worker script.
-        "*.worker.{js,ts}"
-      ],
-      "rules": {
-        "no-console": "error"
-      }
-    }
-  ]
-}

+ 432 - 0
services/web/.eslintrc.js

@@ -0,0 +1,432 @@
+module.exports = {
+  root: true,
+  parser: '@typescript-eslint/parser',
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'standard',
+    'prettier',
+  ],
+  plugins: ['@overleaf'],
+  env: {
+    es2020: true,
+  },
+  settings: {
+    // Tell eslint-plugin-react to detect which version of React we are using
+    react: {
+      version: 'detect',
+    },
+  },
+  rules: {
+    'no-constant-binary-expression': 'error',
+
+    // do not allow importing of implicit dependencies.
+    'import/no-extraneous-dependencies': 'error',
+
+    // disable some TypeScript rules
+    '@typescript-eslint/no-var-requires': 'off',
+    '@typescript-eslint/no-unused-vars': 'off',
+    '@typescript-eslint/no-empty-function': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-this-alias': 'off',
+    '@typescript-eslint/no-non-null-assertion': 'off',
+    '@typescript-eslint/ban-ts-comment': 'off',
+  },
+  overrides: [
+    // NOTE: changing paths may require updating them in the Makefile too.
+    {
+      // Node
+      files: [
+        '**/app/src/**/*.js',
+        'app.js',
+        'i18next-scanner.config.js',
+        'scripts/**/*.js',
+        'webpack.config*.js',
+      ],
+      env: {
+        node: true,
+      },
+    },
+    {
+      // Test specific rules
+      files: ['**/test/**/*.*'],
+      plugins: ['mocha', 'chai-expect', 'chai-friendly'],
+      env: {
+        mocha: true,
+      },
+      rules: {
+        // mocha-specific rules
+        'mocha/handle-done-callback': 'error',
+        'mocha/no-exclusive-tests': 'error',
+        'mocha/no-global-tests': 'error',
+        'mocha/no-identical-title': 'error',
+        'mocha/no-nested-tests': 'error',
+        'mocha/no-pending-tests': 'error',
+        'mocha/no-skipped-tests': 'error',
+        'mocha/no-mocha-arrows': 'error',
+
+        // Swap the no-unused-expressions rule with a more chai-friendly one
+        'no-unused-expressions': 'off',
+        'chai-friendly/no-unused-expressions': 'error',
+
+        // chai-specific rules
+        'chai-expect/missing-assertion': 'error',
+        'chai-expect/terminating-properties': 'error',
+
+        // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
+        // we don't enforce this at the top-level - just in tests to manage `this` scope
+        // based on mocha's context mechanism
+        'mocha/prefer-arrow-callback': 'error',
+      },
+    },
+    {
+      // Backend specific rules
+      files: ['**/app/src/**/*.js', 'app.js'],
+      parserOptions: {
+        tsconfigRootDir: __dirname,
+        project: './tsconfig.backend.json',
+      },
+      rules: {
+        // do not allow importing of implicit dependencies.
+        'import/no-extraneous-dependencies': [
+          'error',
+          {
+            // do not allow importing of devDependencies.
+            devDependencies: false,
+          },
+        ],
+        'no-restricted-syntax': [
+          'error',
+          // do not allow node-fetch in backend code
+          {
+            selector:
+              "CallExpression[callee.name='require'] > .arguments[value='node-fetch']",
+            message:
+              'Requiring node-fetch is not allowed in production services, please use fetch-utils.',
+          },
+          // mongoose populate must set fields to populate
+          {
+            selector:
+              "CallExpression[callee.property.name='populate'][arguments.length<2]",
+            message:
+              "Populate without a second argument returns the whole document. Use populate('field',['prop1','prop2']) instead",
+          },
+          // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
+          {
+            selector:
+              "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
+            message:
+              'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
+          },
+          // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
+          {
+            selector:
+              "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
+            message:
+              "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
+          },
+        ],
+        '@typescript-eslint/no-floating-promises': 'error',
+      },
+    },
+    {
+      // Backend tests and scripts specific rules
+      files: ['**/test/**/*.*', '**/scripts/*.*'],
+      rules: {
+        'no-restricted-syntax': [
+          'error',
+          // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
+          {
+            selector:
+              "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
+            message:
+              'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
+          },
+          // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
+          {
+            selector:
+              "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
+            message:
+              "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
+          },
+          // Catch incorrect usage of `await db.collection.find()`
+          {
+            selector:
+              "AwaitExpression > CallExpression > MemberExpression[property.name='find'][object.object.name='db']",
+            message:
+              'Mongo find returns a cursor not a promise, use `for await (const result of cursor)` or `.toArray()` instead.',
+          },
+        ],
+      },
+    },
+    {
+      // Cypress specific rules
+      files: [
+        'cypress/**/*.{js,jsx,ts,tsx}',
+        '**/test/frontend/**/*.spec.{js,jsx,ts,tsx}',
+      ],
+      extends: ['plugin:cypress/recommended'],
+    },
+    {
+      // Frontend specific rules
+      files: [
+        '**/frontend/js/**/*.{js,jsx,ts,tsx}',
+        '**/frontend/stories/**/*.{js,jsx,ts,tsx}',
+        '**/*.stories.{js,jsx,ts,tsx}',
+        '**/test/frontend/**/*.{js,jsx,ts,tsx}',
+        '**/test/frontend/components/**/*.spec.{js,jsx,ts,tsx}',
+      ],
+      env: {
+        browser: true,
+      },
+      parserOptions: {
+        sourceType: 'module',
+      },
+      plugins: ['jsx-a11y'],
+      extends: [
+        'plugin:react/recommended',
+        'plugin:react-hooks/recommended',
+        'plugin:jsx-a11y/recommended',
+        'standard-jsx',
+        'prettier',
+      ],
+      globals: {
+        __webpack_public_path__: true,
+        $: true,
+        angular: true,
+        ga: true,
+        // Injected in layout.pug
+        user_id: true,
+        ExposedSettings: true,
+      },
+      rules: {
+        // TODO: remove once https://github.com/standard/eslint-config-standard-react/issues/68 (support eslint@8) is fixed.
+        // START: inline standard-react rules
+        // "react/jsx-no-bind": ["error", {
+        //   "allowArrowFunctions": true,
+        //   "allowBind": false,
+        //   "ignoreRefs": true
+        // },],
+        'react/no-did-update-set-state': 'error',
+        'react/no-unused-prop-types': 'error',
+        'react/prop-types': 'error',
+        // "react/react-in-jsx-scope": "error",
+        // END: inline standard-react rules
+
+        'react/no-unknown-property': [
+          'error',
+          {
+            ignore: ['dnd-container', 'dropdown-toggle'],
+          },
+        ],
+
+        'react/jsx-no-target-blank': [
+          'error',
+          {
+            allowReferrer: true,
+          },
+        ],
+        // Prevent usage of legacy string refs
+        'react/no-string-refs': 'error',
+
+        // Prevent curly braces around strings (as they're unnecessary)
+        'react/jsx-curly-brace-presence': [
+          'error',
+          {
+            props: 'never',
+            children: 'never',
+          },
+        ],
+
+        // Don't import React for JSX; the JSX runtime is added by a Babel plugin
+        'react/react-in-jsx-scope': 'off',
+        'react/jsx-uses-react': 'off',
+
+        // Allow functions as JSX props
+        'react/jsx-no-bind': 'off', // TODO: fix occurrences and re-enable this
+
+        // Fix conflict between prettier & standard by overriding to prefer
+        // double quotes
+        'jsx-quotes': ['error', 'prefer-double'],
+
+        // Override weird behaviour of jsx-a11y label-has-for (says labels must be
+        // nested *and* have for/id attributes)
+        'jsx-a11y/label-has-for': [
+          'error',
+          {
+            required: {
+              some: ['nesting', 'id'],
+            },
+          },
+        ],
+
+        // Require .jsx or .tsx file extension when using JSX
+        'react/jsx-filename-extension': [
+          'error',
+          {
+            extensions: ['.jsx', '.tsx'],
+          },
+        ],
+        'no-restricted-syntax': [
+          'error',
+          // Begin: Make sure angular can withstand minification
+          {
+            selector:
+              "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > :function[params.length > 0]",
+            message:
+              "Wrap the function in an array with the parameter names, to withstand minifcation. E.g. App.controller('MyController', ['param1', function(param1) {}]",
+          },
+          {
+            selector:
+              "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > ArrowFunctionExpression",
+            message:
+              'Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}',
+          },
+          {
+            selector:
+              "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrowFunctionExpression",
+            message:
+              'Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}',
+          },
+          {
+            selector:
+              "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > :not(:function, Identifier):last-child",
+            message:
+              "Last element of the array must be a function. E.g ['param1', function(param1) {}]",
+          },
+          {
+            selector:
+              "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression[elements.length=0]",
+            message:
+              "Array must not be empty. Add parameters and a function. E.g ['param1', function(param1) {}]",
+          },
+          // End: Make sure angular can withstand minification
+          // prohibit direct calls to methods of window.localStorage
+          {
+            selector:
+              "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
+            message:
+              'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
+          },
+        ],
+      },
+    },
+    {
+      // React component specific rules
+      //
+      files: [
+        '**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}',
+        '**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}',
+      ],
+      rules: {
+        '@overleaf/no-unnecessary-trans': 'error',
+        '@overleaf/should-unescape-trans': 'error',
+
+        // https://astexplorer.net/
+        'no-restricted-syntax': [
+          'error',
+          // prohibit direct calls to methods of window.location
+          {
+            selector:
+              "CallExpression[callee.object.object.name='window'][callee.object.property.name='location']",
+            message:
+              'Modify location via useLocation instead of calling window.location methods directly',
+          },
+          // prohibit assignment to window.location
+          {
+            selector:
+              "AssignmentExpression[left.object.name='window'][left.property.name='location']",
+            message:
+              'Modify location via useLocation instead of calling window.location methods directly',
+          },
+          // prohibit assignment to window.location.href
+          {
+            selector:
+              "AssignmentExpression[left.object.object.name='window'][left.object.property.name='location'][left.property.name='href']",
+            message:
+              'Modify location via useLocation instead of calling window.location methods directly',
+          },
+          // prohibit using lookbehinds due to incidents with Safari simply crashing when the script is parsed
+          {
+            selector: 'Literal[regex.pattern=/\\(\\?<[!=]/]',
+            message: 'Lookbehind is not supported in older Safari versions.',
+          },
+          // prohibit direct calls to methods of window.localStorage
+          // NOTE: this rule is also defined for all frontend files, but those rules are overriden by the React component-specific config
+          {
+            selector:
+              "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
+            message:
+              'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
+          },
+        ],
+      },
+    },
+    // React + TypeScript-specific rules
+    {
+      files: ['**/*.tsx'],
+      rules: {
+        'react/prop-types': 'off',
+        'no-undef': 'off',
+      },
+    },
+    // TypeScript-specific rules
+    {
+      files: ['**/*.ts'],
+      rules: {
+        'no-undef': 'off',
+      },
+    },
+    {
+      files: ['scripts/ukamf/*.js'],
+      rules: {
+        // Do not allow importing of any dependencies unless specified in either
+        //  - web/package.json
+        //  - web/scripts/ukamf/package.json
+        'import/no-extraneous-dependencies': [
+          'error',
+          { packageDir: ['.', 'scripts/ukamf'] },
+        ],
+      },
+    },
+    {
+      files: ['scripts/learn/checkSanitize/*.js'],
+      rules: {
+        // The checkSanitize script is used in the dev-env only.
+        'import/no-extraneous-dependencies': [
+          'error',
+          {
+            devDependencies: true,
+            packageDir: ['.', '../../'],
+          },
+        ],
+      },
+    },
+    {
+      files: [
+        // Backend: Use @overleaf/logger
+        //          Docs: https://manual.dev-overleaf.com/development/code/logging/#structured-logging
+        '**/app/**/*.{js,cjs,mjs}',
+        'app.js',
+        'modules/*/*.js',
+        // Frontend: Prefer debugConsole over bare console
+        //           Docs: https://manual.dev-overleaf.com/development/code/logging/#frontend
+        '**/frontend/**/*.{js,jsx,ts,tsx}',
+        // Tests
+        '**/test/**/*.{js,cjs,mjs,jsx,ts,tsx}',
+      ],
+      excludedFiles: [
+        // Allow console logs in scripts
+        '**/scripts/**/*.js',
+        // Allow console logs in stories
+        '**/stories/**/*.{js,jsx,ts,tsx}',
+        // Workers do not have access to the search params for enabling ?debug=true.
+        // self.location.url is the URL of the worker script.
+        '*.worker.{js,ts}',
+      ],
+      rules: {
+        'no-console': 'error',
+      },
+    },
+  ],
+}

+ 31 - 0
services/web/app/src/Features/Analytics/AnalyticsManager.js

@@ -57,6 +57,15 @@ async function recordEventForUser(userId, event, segmentation) {
   }
 }
 
+function recordEventForUserInBackground(userId, event, segmentation) {
+  recordEventForUser(userId, event, segmentation).catch(err => {
+    logger.warn(
+      { err, userId, event, segmentation },
+      'failed to record event for user'
+    )
+  })
+}
+
 function recordEventForSession(session, event, segmentation) {
   const { analyticsId, userId } = getIdsFromSession(session)
   if (!analyticsId) {
@@ -88,6 +97,15 @@ async function setUserPropertyForUser(userId, propertyName, propertyValue) {
   }
 }
 
+function setUserPropertyForUserInBackground(userId, property, value) {
+  setUserPropertyForUser(userId, property, value).catch(err => {
+    logger.warn(
+      { err, userId, property, value },
+      'failed to set user property for user'
+    )
+  })
+}
+
 async function setUserPropertyForAnalyticsId(
   analyticsId,
   propertyName,
@@ -115,6 +133,16 @@ async function setUserPropertyForSession(session, propertyName, propertyValue) {
   }
 }
 
+function setUserPropertyForSessionInBackground(session, property, value) {
+  setUserPropertyForSession(session, property, value).catch(err => {
+    const { analyticsId, userId } = getIdsFromSession(session)
+    logger.warn(
+      { err, analyticsId, userId, property, value },
+      'failed to set user property for session'
+    )
+  })
+}
+
 function updateEditingSession(userId, projectId, countryCode, segmentation) {
   if (!userId) {
     return
@@ -310,8 +338,11 @@ module.exports = {
   identifyUser,
   recordEventForSession,
   recordEventForUser,
+  recordEventForUserInBackground,
   setUserPropertyForUser,
+  setUserPropertyForUserInBackground,
   setUserPropertyForSession,
+  setUserPropertyForSessionInBackground,
   setUserPropertyForAnalyticsId,
   updateEditingSession,
   getIdsFromSession,

+ 7 - 7
services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.js

@@ -31,25 +31,25 @@ function addUserProperties(userId, session) {
   }
 
   if (session.required_login_from_product_medium) {
-    AnalyticsManager.setUserPropertyForUser(
+    AnalyticsManager.setUserPropertyForUserInBackground(
       userId,
       `registered-from-product-medium`,
       session.required_login_from_product_medium
     )
     if (session.required_login_from_product_source) {
-      AnalyticsManager.setUserPropertyForUser(
+      AnalyticsManager.setUserPropertyForUserInBackground(
         userId,
         `registered-from-product-source`,
         session.required_login_from_product_source
       )
     }
   } else if (session.referal_id) {
-    AnalyticsManager.setUserPropertyForUser(
+    AnalyticsManager.setUserPropertyForUserInBackground(
       userId,
       `registered-from-bonus-scheme`,
       true
     )
-    AnalyticsManager.setUserPropertyForUser(
+    AnalyticsManager.setUserPropertyForUserInBackground(
       userId,
       `registered-from-product-medium`,
       'bonus-scheme'
@@ -58,7 +58,7 @@ function addUserProperties(userId, session) {
 
   if (session.inbound) {
     if (session.inbound.referrer && session.inbound.referrer.medium) {
-      AnalyticsManager.setUserPropertyForUser(
+      AnalyticsManager.setUserPropertyForUserInBackground(
         userId,
         `registered-from-referrer-medium`,
         `${session.inbound.referrer.medium
@@ -66,7 +66,7 @@ function addUserProperties(userId, session) {
           .toUpperCase()}${session.inbound.referrer.medium.slice(1)}`
       )
       if (session.inbound.referrer.source) {
-        AnalyticsManager.setUserPropertyForUser(
+        AnalyticsManager.setUserPropertyForUserInBackground(
           userId,
           `registered-from-referrer-source`,
           session.inbound.referrer.source
@@ -77,7 +77,7 @@ function addUserProperties(userId, session) {
     if (session.inbound.utm) {
       for (const utmKey of RequestHelper.UTM_KEYS) {
         if (session.inbound.utm[utmKey]) {
-          AnalyticsManager.setUserPropertyForUser(
+          AnalyticsManager.setUserPropertyForUserInBackground(
             userId,
             `registered-from-${utmKey.replace('_', '-')}`,
             session.inbound.utm[utmKey]

+ 1 - 1
services/web/app/src/Features/Analytics/AnalyticsUTMTrackingMiddleware.js

@@ -27,7 +27,7 @@ function recordUTMTags() {
         };${utmValues.utm_campaign || 'N/A'};${
           utmValues.utm_content || utmValues.utm_term || 'N/A'
         }`
-        AnalyticsManager.setUserPropertyForSession(
+        AnalyticsManager.setUserPropertyForSessionInBackground(
           req.session,
           'utm-tags',
           propertyValue

+ 1 - 1
services/web/app/src/Features/Authentication/AuthenticationController.js

@@ -646,7 +646,7 @@ function _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser) {
   LoginRateLimiter.recordSuccessfulLogin(user.email, () => {})
   AuthenticationController._recordSuccessfulLogin(user._id, () => {})
   AuthenticationController.ipMatchCheck(req, user)
-  Analytics.recordEventForUser(user._id, 'user-logged-in', {
+  Analytics.recordEventForUserInBackground(user._id, 'user-logged-in', {
     source: req.session.saml
       ? 'saml'
       : req.user_info?.auth_provider || 'email-password',

+ 10 - 2
services/web/app/src/Features/BetaProgram/BetaProgramHandler.js

@@ -6,7 +6,11 @@ const AnalyticsManager = require('../Analytics/AnalyticsManager')
 async function optIn(userId) {
   await UserUpdater.promises.updateUser(userId, { $set: { betaProgram: true } })
   metrics.inc('beta-program.opt-in')
-  AnalyticsManager.setUserPropertyForUser(userId, 'beta-program', true)
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'beta-program',
+    true
+  )
 }
 
 async function optOut(userId) {
@@ -14,7 +18,11 @@ async function optOut(userId) {
     $set: { betaProgram: false },
   })
   metrics.inc('beta-program.opt-out')
-  AnalyticsManager.setUserPropertyForUser(userId, 'beta-program', false)
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'beta-program',
+    false
+  )
 }
 
 module.exports = {

+ 1 - 1
services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js

@@ -348,7 +348,7 @@ const CollaboratorsInviteController = {
       'project:membership:changed',
       { invites: true, members: true }
     )
-    AnalyticsManager.recordEventForUser(
+    AnalyticsManager.recordEventForUserInBackground(
       currentUser._id,
       'project-invite-accept',
       {

+ 1 - 1
services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js

@@ -158,7 +158,7 @@ const CollaboratorsInviteHandler = {
   },
 
   async acceptInvite(invite, projectId, user) {
-    CollaboratorsHandler.promises.addUserIdToProject(
+    await CollaboratorsHandler.promises.addUserIdToProject(
       projectId,
       invite.sendingUserId,
       user._id,

+ 1 - 1
services/web/app/src/Features/Collaborators/OwnershipTransferHandler.js

@@ -38,7 +38,7 @@ async function transferOwnership(projectId, newOwnerId, options = {}) {
   }
 
   // Track the change of ownership in BigQuery.
-  AnalyticsManager.recordEventForUser(
+  AnalyticsManager.recordEventForUserInBackground(
     previousOwnerId,
     'project-ownership-transfer',
     { projectId, newOwnerId }

+ 4 - 1
services/web/app/src/Features/Email/EmailHandler.js

@@ -1,5 +1,6 @@
 const { callbackify } = require('util')
 const Settings = require('@overleaf/settings')
+const logger = require('@overleaf/logger')
 const EmailBuilder = require('./EmailBuilder')
 const EmailSender = require('./EmailSender')
 const Queues = require('../../infrastructure/Queues')
@@ -30,5 +31,7 @@ function sendDeferredEmail(emailType, opts, delay) {
     'deferred-emails',
     { data: { emailType, opts } },
     delay
-  )
+  ).catch(err => {
+    logger.warn({ err, emailType, opts }, 'failed to queue deferred email')
+  })
 }

+ 7 - 3
services/web/app/src/Features/LinkedFiles/LinkedFilesController.js

@@ -94,9 +94,13 @@ module.exports = LinkedFilesController = {
           return LinkedFilesController.handleError(err, req, res, next)
         }
         if (name.endsWith('.bib')) {
-          AnalyticsManager.recordEventForUser(userId, 'linked-bib-file', {
-            integration: provider,
-          })
+          AnalyticsManager.recordEventForUserInBackground(
+            userId,
+            'linked-bib-file',
+            {
+              integration: provider,
+            }
+          )
         }
         return res.json({ new_file_id: newFileId })
       }

+ 11 - 3
services/web/app/src/Features/Project/ProjectController.js

@@ -435,12 +435,16 @@ const ProjectController = {
             SplitTestSessionHandler.sessionMaintenance(req, null, () => {})
             cb(null, defaultSettingsForAnonymousUser(userId))
           } else {
+            // Ignore spurious floating promises warning until we promisify
+            // eslint-disable-next-line @typescript-eslint/no-floating-promises
             User.updateOne(
               { _id: new ObjectId(userId) },
               { $set: { lastActive: new Date() } },
               {},
               () => {}
             )
+            // Ignore spurious floating promises warning until we promisify
+            // eslint-disable-next-line @typescript-eslint/no-floating-promises
             User.findById(
               userId,
               'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram completedTutorials writefull',
@@ -693,9 +697,13 @@ const ProjectController = {
             metrics.inc(metricName)
 
             if (userId) {
-              AnalyticsManager.recordEventForUser(userId, 'project-opened', {
-                projectId: project._id,
-              })
+              AnalyticsManager.recordEventForUserInBackground(
+                userId,
+                'project-opened',
+                {
+                  projectId: project._id,
+                }
+              )
             }
 
             // should not be used in place of split tests query param overrides (?my-split-test-name=my-variant)

+ 5 - 5
services/web/app/src/Features/Project/ProjectCreationHandler.js

@@ -55,13 +55,13 @@ async function createBlankProject(
   Object.assign(segmentation, attributes.segmentation)
   segmentation.projectId = project._id
   if (isImport) {
-    AnalyticsManager.recordEventForUser(
+    AnalyticsManager.recordEventForUserInBackground(
       ownerId,
       'project-imported',
       segmentation
     )
   } else {
-    AnalyticsManager.recordEventForUser(
+    AnalyticsManager.recordEventForUserInBackground(
       ownerId,
       'project-created',
       segmentation
@@ -72,7 +72,7 @@ async function createBlankProject(
 
 async function createProjectFromSnippet(ownerId, projectName, docLines) {
   const project = await _createBlankProject(ownerId, projectName)
-  AnalyticsManager.recordEventForUser(ownerId, 'project-created', {
+  AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
     projectId: project._id,
   })
   await _createRootDoc(project, ownerId, docLines)
@@ -85,7 +85,7 @@ async function createBasicProject(ownerId, projectName) {
   const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName)
   await _createRootDoc(project, ownerId, docLines)
 
-  AnalyticsManager.recordEventForUser(ownerId, 'project-created', {
+  AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
     projectId: project._id,
   })
 
@@ -97,7 +97,7 @@ async function createExampleProject(ownerId, projectName) {
 
   await _addExampleProjectFiles(ownerId, projectName, project)
 
-  AnalyticsManager.recordEventForUser(ownerId, 'project-created', {
+  AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
     projectId: project._id,
   })
 

+ 4 - 0
services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js

@@ -245,6 +245,8 @@ const ProjectEntityUpdateHandler = {
           return callback(err)
         }
         if (ProjectEntityUpdateHandler.isPathValidForRootDoc(docPath)) {
+          // Ignore spurious floating promises warning until we promisify
+          // eslint-disable-next-line @typescript-eslint/no-floating-promises
           Project.updateOne(
             { _id: projectId },
             { rootDoc_id: newRootDocID },
@@ -264,6 +266,8 @@ const ProjectEntityUpdateHandler = {
 
   unsetRootDoc(projectId, callback) {
     logger.debug({ projectId }, 'removing root doc')
+    // Ignore spurious floating promises warning until we promisify
+    // eslint-disable-next-line @typescript-eslint/no-floating-promises
     Project.updateOne(
       { _id: projectId },
       { $unset: { rootDoc_id: true } },

+ 27 - 3
services/web/app/src/Features/SplitTests/SplitTestHandler.js

@@ -314,7 +314,19 @@ async function _getAssignment(
         if (sync === true) {
           await _recordAssignment(assignmentData)
         } else {
-          _recordAssignment(assignmentData)
+          _recordAssignment(assignmentData).catch(err => {
+            logger.warn(
+              {
+                err,
+                userId,
+                splitTestName,
+                phase,
+                versionNumber,
+                variantName: selectedVariantName,
+              },
+              'failed to record split test assignment'
+            )
+          })
         }
       }
       // otherwise this is an anonymous user, we store assignments in session to persist them on registration
@@ -329,11 +341,23 @@ async function _getAssignment(
         })
       }
 
+      const effectiveAnalyticsId = user?.analyticsId || analyticsId || userId
       AnalyticsManager.setUserPropertyForAnalyticsId(
-        user?.analyticsId || analyticsId || userId,
+        effectiveAnalyticsId,
         `split-test-${splitTestName}-${versionNumber}`,
         selectedVariantName
-      )
+      ).catch(err => {
+        logger.warn(
+          {
+            err,
+            analyticsId: effectiveAnalyticsId,
+            splitTest: splitTestName,
+            versionNumber,
+            variant: selectedVariantName,
+          },
+          'failed to set user property for analytics id'
+        )
+      })
     }
     return _makeAssignment(splitTest, selectedVariantName, currentVersion)
   }

+ 1 - 1
services/web/app/src/Features/Subscription/FeaturesUpdater.js

@@ -43,7 +43,7 @@ async function refreshFeatures(userId, reason) {
   logger.debug({ userId, features }, 'updating user features')
 
   const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features)
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'feature-set',
     matchedFeatureSet

+ 102 - 54
services/web/app/src/Features/Subscription/RecurlyEventHandler.js

@@ -51,19 +51,27 @@ async function sendRecurlyAnalyticsEvent(event, eventData) {
 async function _sendSubscriptionStartedEvent(userId, eventData) {
   const { planCode, quantity, state, isTrial, subscriptionId } =
     _getSubscriptionData(eventData)
-  AnalyticsManager.recordEventForUser(userId, 'subscription-started', {
-    plan_code: planCode,
-    quantity,
-    is_trial: isTrial,
-    subscriptionId,
-  })
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'subscription-started',
+    {
+      plan_code: planCode,
+      quantity,
+      is_trial: isTrial,
+      subscriptionId,
+    }
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-plan-code',
     planCode
   )
-  AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'subscription-state',
+    state
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-is-trial',
     isTrial
@@ -77,19 +85,27 @@ async function _sendSubscriptionStartedEvent(userId, eventData) {
 async function _sendSubscriptionUpdatedEvent(userId, eventData) {
   const { planCode, quantity, state, isTrial, subscriptionId } =
     _getSubscriptionData(eventData)
-  AnalyticsManager.recordEventForUser(userId, 'subscription-updated', {
-    plan_code: planCode,
-    quantity,
-    is_trial: isTrial,
-    subscriptionId,
-  })
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'subscription-updated',
+    {
+      plan_code: planCode,
+      quantity,
+      is_trial: isTrial,
+      subscriptionId,
+    }
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-plan-code',
     planCode
   )
-  AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'subscription-state',
+    state
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-is-trial',
     isTrial
@@ -99,14 +115,22 @@ async function _sendSubscriptionUpdatedEvent(userId, eventData) {
 async function _sendSubscriptionCancelledEvent(userId, eventData) {
   const { planCode, quantity, state, isTrial, subscriptionId } =
     _getSubscriptionData(eventData)
-  AnalyticsManager.recordEventForUser(userId, 'subscription-cancelled', {
-    plan_code: planCode,
-    quantity,
-    is_trial: isTrial,
-    subscriptionId,
-  })
-  AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'subscription-cancelled',
+    {
+      plan_code: planCode,
+      quantity,
+      is_trial: isTrial,
+      subscriptionId,
+    }
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'subscription-state',
+    state
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-is-trial',
     isTrial
@@ -116,19 +140,27 @@ async function _sendSubscriptionCancelledEvent(userId, eventData) {
 async function _sendSubscriptionExpiredEvent(userId, eventData) {
   const { planCode, quantity, state, isTrial, subscriptionId } =
     _getSubscriptionData(eventData)
-  AnalyticsManager.recordEventForUser(userId, 'subscription-expired', {
-    plan_code: planCode,
-    quantity,
-    is_trial: isTrial,
-    subscriptionId,
-  })
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'subscription-expired',
+    {
+      plan_code: planCode,
+      quantity,
+      is_trial: isTrial,
+      subscriptionId,
+    }
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-plan-code',
     planCode
   )
-  AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'subscription-state',
+    state
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-is-trial',
     isTrial
@@ -138,19 +170,27 @@ async function _sendSubscriptionExpiredEvent(userId, eventData) {
 async function _sendSubscriptionRenewedEvent(userId, eventData) {
   const { planCode, quantity, state, isTrial, subscriptionId } =
     _getSubscriptionData(eventData)
-  AnalyticsManager.recordEventForUser(userId, 'subscription-renewed', {
-    plan_code: planCode,
-    quantity,
-    is_trial: isTrial,
-    subscriptionId,
-  })
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'subscription-renewed',
+    {
+      plan_code: planCode,
+      quantity,
+      is_trial: isTrial,
+      subscriptionId,
+    }
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-plan-code',
     planCode
   )
-  AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'subscription-state',
+    state
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-is-trial',
     isTrial
@@ -160,18 +200,26 @@ async function _sendSubscriptionRenewedEvent(userId, eventData) {
 async function _sendSubscriptionReactivatedEvent(userId, eventData) {
   const { planCode, quantity, state, isTrial, subscriptionId } =
     _getSubscriptionData(eventData)
-  AnalyticsManager.recordEventForUser(userId, 'subscription-reactivated', {
-    plan_code: planCode,
-    quantity,
-    subscriptionId,
-  })
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'subscription-reactivated',
+    {
+      plan_code: planCode,
+      quantity,
+      subscriptionId,
+    }
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-plan-code',
     planCode
   )
-  AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.setUserPropertyForUserInBackground(
+    userId,
+    'subscription-state',
+    state
+  )
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-is-trial',
     isTrial
@@ -195,7 +243,7 @@ async function _sendInvoicePaidEvent(userId, eventData) {
       subscriptionIds[`subscriptionId${idx + 1}`] = e
     }
   })
-  AnalyticsManager.recordEventForUser(
+  AnalyticsManager.recordEventForUserInBackground(
     userId,
     'subscription-invoice-collected',
     {
@@ -208,7 +256,7 @@ async function _sendInvoicePaidEvent(userId, eventData) {
       ...subscriptionIds,
     }
   )
-  AnalyticsManager.setUserPropertyForUser(
+  AnalyticsManager.setUserPropertyForUserInBackground(
     userId,
     'subscription-is-trial',
     false

+ 3 - 3
services/web/app/src/Features/Subscription/SubscriptionUpdater.js

@@ -355,7 +355,7 @@ async function _sendUserGroupPlanCodeUserProperty(userId) {
         bestFeatures = plan.features
       }
     }
-    AnalyticsManager.setUserPropertyForUser(
+    AnalyticsManager.setUserPropertyForUserInBackground(
       userId,
       'group-subscription-plan-code',
       bestPlanCode
@@ -376,7 +376,7 @@ async function _sendSubscriptionEvent(userId, subscriptionId, event) {
   if (!subscription || !subscription.groupPlan) {
     return
   }
-  AnalyticsManager.recordEventForUser(userId, event, {
+  AnalyticsManager.recordEventForUserInBackground(userId, event, {
     groupId: subscription._id.toString(),
     subscriptionId: subscription.recurlySubscription_id,
   })
@@ -397,7 +397,7 @@ async function _sendSubscriptionEventForAllMembers(subscriptionId, event) {
   const userIds = (subscription.member_ids || []).filter(Boolean)
   for (const userId of userIds) {
     if (userId) {
-      AnalyticsManager.recordEventForUser(userId, event, {
+      AnalyticsManager.recordEventForUserInBackground(userId, event, {
         groupId: subscription._id.toString(),
         subscriptionId: subscription.recurlySubscription_id,
       })

+ 9 - 3
services/web/app/src/Features/SystemMessages/SystemMessageManager.js

@@ -3,6 +3,7 @@ const {
   addRequiredCleanupHandlerBeforeDrainingConnections,
 } = require('../../infrastructure/GracefulShutdown')
 const { callbackifyAll } = require('@overleaf/promise-utils')
+const logger = require('@overleaf/logger')
 
 const SystemMessageManager = {
   getMessages() {
@@ -22,9 +23,14 @@ const SystemMessageManager = {
     await message.save()
   },
 
-  async refreshCache() {
-    const messages = await this.getMessagesFromDB()
-    this._cachedMessages = messages
+  refreshCache() {
+    this.getMessagesFromDB()
+      .then(messages => {
+        this._cachedMessages = messages
+      })
+      .catch(err => {
+        logger.warn({ err }, 'failed to refresh system messages cache')
+      })
   },
 }
 

+ 2 - 2
services/web/app/src/Features/TokenAccess/TokenAccessHandler.js

@@ -154,7 +154,7 @@ const TokenAccessHandler = {
   async addReadOnlyUserToProject(userId, projectId) {
     userId = new ObjectId(userId.toString())
     projectId = new ObjectId(projectId.toString())
-    Analytics.recordEventForUser(userId, 'project-joined', {
+    Analytics.recordEventForUserInBackground(userId, 'project-joined', {
       mode: 'read-only',
     })
 
@@ -171,7 +171,7 @@ const TokenAccessHandler = {
   async addReadAndWriteUserToProject(userId, projectId) {
     userId = new ObjectId(userId.toString())
     projectId = new ObjectId(projectId.toString())
-    Analytics.recordEventForUser(userId, 'project-joined', {
+    Analytics.recordEventForUserInBackground(userId, 'project-joined', {
       mode: 'read-write',
     })
 

+ 9 - 9
services/web/app/src/Features/User/ThirdPartyIdentityManager.js

@@ -165,21 +165,21 @@ function _getUserQuery(providerId, externalUserId) {
   return query
 }
 
-async function _sendSecurityAlert(accountLinked, providerId, user, userId) {
+function _sendSecurityAlert(accountLinked, providerId, user, userId) {
   const providerName = oauthProviders[providerId].name
   const emailOptions = EmailOptionsHelper.linkOrUnlink(
     accountLinked,
     providerName,
     user.email
   )
-  try {
-    await EmailHandler.promises.sendEmail('securityAlert', emailOptions)
-  } catch (error) {
-    logger.error(
-      { err: error, userId },
-      `could not send security alert email when ${emailOptions.action.toLowerCase()}`
-    )
-  }
+  EmailHandler.promises
+    .sendEmail('securityAlert', emailOptions)
+    .catch(error => {
+      logger.error(
+        { err: error, userId },
+        `could not send security alert email when ${emailOptions.action.toLowerCase()}`
+      )
+    })
 }
 
 function _thirdPartyIdentifierUpdate(

+ 5 - 1
services/web/app/src/Features/User/UserCreator.js

@@ -43,7 +43,11 @@ async function recordRegistrationEvent(user) {
     if (user.thirdPartyIdentifiers && user.thirdPartyIdentifiers.length > 0) {
       segmentation.provider = user.thirdPartyIdentifiers[0].providerId
     }
-    Analytics.recordEventForUser(user._id, 'user-registered', segmentation)
+    Analytics.recordEventForUserInBackground(
+      user._id,
+      'user-registered',
+      segmentation
+    )
   } catch (err) {
     logger.warn({ err }, 'there was an error recording `user-registered` event')
   }

+ 15 - 8
services/web/app/src/Features/User/UserEmailsController.js

@@ -319,11 +319,15 @@ async function checkSecondaryEmailConfirmationCode(req, res) {
 
     delete req.session.pendingSecondaryEmail
 
-    AnalyticsManager.recordEventForUser(user._id, 'email-verified', {
-      provider: 'email',
-      verification_type: 'token',
-      isPrimary: false,
-    })
+    AnalyticsManager.recordEventForUserInBackground(
+      user._id,
+      'email-verified',
+      {
+        provider: 'email',
+        verification_type: 'token',
+        isPrimary: false,
+      }
+    )
 
     const redirectUrl =
       AuthenticationController.getRedirectFromSession(req) || '/project'
@@ -427,7 +431,7 @@ async function primaryEmailCheckPage(req, res) {
     return res.redirect('/project')
   }
 
-  AnalyticsManager.recordEventForUser(
+  AnalyticsManager.recordEventForUserInBackground(
     userId,
     'primary-email-check-page-displayed'
   )
@@ -439,7 +443,10 @@ async function primaryEmailCheck(req, res) {
   await UserUpdater.promises.updateUser(userId, {
     $set: { lastPrimaryEmailCheck: new Date() },
   })
-  AnalyticsManager.recordEventForUser(userId, 'primary-email-check-done')
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'primary-email-check-done'
+  )
   AsyncFormHelper.redirect(req, res, '/project')
 }
 
@@ -610,7 +617,7 @@ const UserEmailsController = {
                     )
                   }
                   const isPrimary = user?.email === userData.email
-                  AnalyticsManager.recordEventForUser(
+                  AnalyticsManager.recordEventForUserInBackground(
                     userData.userId,
                     'email-verified',
                     {

+ 5 - 5
services/web/app/src/Features/User/UserRegistrationHandler.js

@@ -113,14 +113,14 @@ const UserRegistrationHandler = {
 
     const setNewPasswordUrl = `${settings.siteUrl}/user/activate?token=${token}&user_id=${user._id}`
 
-    try {
-      await EmailHandler.promises.sendEmail('registered', {
+    EmailHandler.promises
+      .sendEmail('registered', {
         to: user.email,
         setNewPasswordUrl,
       })
-    } catch (error) {
-      logger.warn({ err: error }, 'failed to send activation email')
-    }
+      .catch(error => {
+        logger.warn({ err: error }, 'failed to send activation email')
+      })
 
     return { user, setNewPasswordUrl }
   },

+ 8 - 2
services/web/app/src/Features/User/UserUpdater.js

@@ -75,7 +75,10 @@ async function addEmailAddress(userId, newEmail, affiliationOptions, auditLog) {
 
   await UserGetter.promises.ensureUniqueEmailAddress(newEmail)
 
-  AnalyticsManager.recordEventForUser(userId, 'secondary-email-added')
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'secondary-email-added'
+  )
 
   await UserAuditLogHandler.promises.addEntry(
     userId,
@@ -201,7 +204,10 @@ async function setDefaultEmailAddress(
     throw new Error('email update error')
   }
 
-  AnalyticsManager.recordEventForUser(userId, 'primary-email-address-updated')
+  AnalyticsManager.recordEventForUserInBackground(
+    userId,
+    'primary-email-address-updated'
+  )
 
   if (sendSecurityAlert) {
     // no need to wait, errors are logged and not passed back

+ 3 - 0
services/web/app/src/infrastructure/Translations.js

@@ -76,6 +76,9 @@ i18n
     supportedLngs: availableLanguageCodes,
     fallbackLng: fallbackLanguageCode,
   })
+  .catch(err => {
+    logger.error({ err }, 'failed to initialize i18next library')
+  })
 
 // Make custom language detector for Accept-Language header
 const headerLangDetector = new middleware.LanguageDetector(i18n.services, {

+ 4 - 0
services/web/modules/launchpad/app/src/LaunchpadController.js

@@ -169,6 +169,8 @@ module.exports = LaunchpadController = {
             return next(err)
           }
 
+          // Ignore spurious floating promises warning until we promisify
+          // eslint-disable-next-line @typescript-eslint/no-floating-promises
           User.updateOne(
             { _id: user._id },
             {
@@ -245,6 +247,8 @@ module.exports = LaunchpadController = {
         }
 
         logger.debug({ userId: user._id }, 'making user an admin')
+        // Ignore spurious floating promises warning until we promisify
+        // eslint-disable-next-line @typescript-eslint/no-floating-promises
         User.updateOne(
           { _id: user._id },
           {

+ 2 - 2
services/web/package.json

@@ -245,8 +245,8 @@
     "@types/recurly__recurly-js": "^4.22.0",
     "@types/sinon-chai": "^3.2.8",
     "@types/uuid": "^9.0.8",
-    "@typescript-eslint/eslint-plugin": "^6.7.4",
-    "@typescript-eslint/parser": "^6.7.4",
+    "@typescript-eslint/eslint-plugin": "^7.8.0",
+    "@typescript-eslint/parser": "^7.8.0",
     "@uppy/core": "^3.8.0",
     "@uppy/dashboard": "^3.7.1",
     "@uppy/drag-drop": "^3.0.3",

+ 1 - 1
services/web/scripts/refresh_features.js

@@ -81,7 +81,7 @@ const checkAndUpdateUser = (user, callback) =>
     }
 
     const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(freshFeatures)
-    AnalyticsManager.setUserPropertyForUser(
+    AnalyticsManager.setUserPropertyForUserInBackground(
       user._id,
       'feature-set',
       matchedFeatureSet

+ 7 - 5
services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddlewareTests.js

@@ -29,7 +29,7 @@ describe('AnalyticsUTMTrackingMiddleware', function () {
       requires: {
         './AnalyticsManager': (this.AnalyticsManager = {
           recordEventForSession: sinon.stub().resolves(),
-          setUserPropertyForSession: sinon.stub().resolves(),
+          setUserPropertyForSessionInBackground: sinon.stub(),
         }),
         '@overleaf/settings': {
           siteUrl: 'https://www.overleaf.com',
@@ -56,7 +56,9 @@ describe('AnalyticsUTMTrackingMiddleware', function () {
 
     it('no event or user property is recorded', function () {
       sinon.assert.notCalled(this.AnalyticsManager.recordEventForSession)
-      sinon.assert.notCalled(this.AnalyticsManager.setUserPropertyForSession)
+      sinon.assert.notCalled(
+        this.AnalyticsManager.setUserPropertyForSessionInBackground
+      )
     })
   })
 
@@ -101,7 +103,7 @@ describe('AnalyticsUTMTrackingMiddleware', function () {
 
     it('utm-tags user property is set for session', function () {
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForSession,
+        this.AnalyticsManager.setUserPropertyForSessionInBackground,
         this.req.session,
         'utm-tags',
         'Organic;Facebook;Some Campaign;foo-bar'
@@ -146,7 +148,7 @@ describe('AnalyticsUTMTrackingMiddleware', function () {
 
     it('utm-tags user property is set for session', function () {
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForSession,
+        this.AnalyticsManager.setUserPropertyForSessionInBackground,
         this.req.session,
         'utm-tags',
         'N/A;Facebook;Some Campaign;foo'
@@ -190,7 +192,7 @@ describe('AnalyticsUTMTrackingMiddleware', function () {
 
     it('utm-tags user property is set for session', function () {
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForSession,
+        this.AnalyticsManager.setUserPropertyForSessionInBackground,
         this.req.session,
         'utm-tags',
         'N/A;Facebook;Some Campaign;N/A'

+ 2 - 2
services/web/test/unit/src/Authentication/AuthenticationControllerTests.js

@@ -88,7 +88,7 @@ describe('AuthenticationController', function () {
           setupLoginData: sinon.stub(),
         }),
         '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
-          recordEventForUser: sinon.stub(),
+          recordEventForUserInBackground: sinon.stub(),
           identifyUser: sinon.stub(),
           getIdsFromSession: sinon.stub().returns({ userId: this.user._id }),
         }),
@@ -1476,7 +1476,7 @@ describe('AuthenticationController', function () {
 
       it('should track the login event', function () {
         sinon.assert.calledWith(
-          this.AnalyticsManager.recordEventForUser,
+          this.AnalyticsManager.recordEventForUserInBackground,
           this.user._id,
           'user-logged-in'
         )

+ 3 - 3
services/web/test/unit/src/BetaProgram/BetaProgramHandlerTests.js

@@ -28,7 +28,7 @@ describe('BetaProgramHandler', function () {
           },
         }),
         '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
-          setUserPropertyForUser: sinon.stub().resolves(),
+          setUserPropertyForUserInBackground: sinon.stub(),
         }),
       },
     })
@@ -54,7 +54,7 @@ describe('BetaProgramHandler', function () {
       this.call(err => {
         expect(err).to.not.exist
         sinon.assert.calledWith(
-          this.AnalyticsManager.setUserPropertyForUser,
+          this.AnalyticsManager.setUserPropertyForUserInBackground,
           this.user_id,
           'beta-program',
           true
@@ -105,7 +105,7 @@ describe('BetaProgramHandler', function () {
       this.call(err => {
         expect(err).to.not.exist
         sinon.assert.calledWith(
-          this.AnalyticsManager.setUserPropertyForUser,
+          this.AnalyticsManager.setUserPropertyForUserInBackground,
           this.user_id,
           'beta-program',
           false

+ 1 - 1
services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js

@@ -33,7 +33,7 @@ describe('CollaboratorsInviteController', function () {
       getSessionUser: sinon.stub().returns(this.currentUser),
     }
 
-    this.AnalyticsManger = { recordEventForUser: sinon.stub() }
+    this.AnalyticsManger = { recordEventForUserInBackground: sinon.stub() }
 
     this.rateLimiter = {
       consume: sinon.stub().resolves(),

+ 3 - 2
services/web/test/unit/src/Collaborators/OwnershipTransferHandlerTests.js

@@ -74,7 +74,8 @@ describe('OwnershipTransferHandler', function () {
         '../Email/EmailHandler': this.EmailHandler,
         './CollaboratorsHandler': this.CollaboratorsHandler,
         '../Analytics/AnalyticsManager': {
-          recordEventForUser: (this.recordEventForUser = sinon.stub()),
+          recordEventForUserInBackground: (this.recordEventForUserInBackground =
+            sinon.stub()),
         },
       },
     })
@@ -214,7 +215,7 @@ describe('OwnershipTransferHandler', function () {
         this.collaborator._id,
         { sessionUserId }
       )
-      expect(this.recordEventForUser).to.have.been.calledWith(
+      expect(this.recordEventForUserInBackground).to.have.been.calledWith(
         this.user._id,
         'project-ownership-transfer',
         {

+ 1 - 1
services/web/test/unit/src/Email/EmailHandlerTests.js

@@ -21,7 +21,7 @@ describe('EmailHandler', function () {
       },
     }
     this.Queues = {
-      createScheduledJob: sinon.stub(),
+      createScheduledJob: sinon.stub().resolves(),
     }
     this.EmailHandler = SandboxedModule.require(MODULE_PATH, {
       requires: {

+ 3 - 1
services/web/test/unit/src/Project/ProjectControllerTests.js

@@ -189,7 +189,9 @@ describe('ProjectController', function () {
           this.BrandVariationsHandler,
         '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
         '../../models/Project': {},
-        '../Analytics/AnalyticsManager': { recordEventForUser: () => {} },
+        '../Analytics/AnalyticsManager': {
+          recordEventForUserInBackground: () => {},
+        },
         '../Subscription/SubscriptionViewModelBuilder':
           this.SubscriptionViewModelBuilder,
         '../Spelling/SpellingHandler': {

+ 1 - 1
services/web/test/unit/src/SplitTests/SplitTestHandlerTests.js

@@ -48,7 +48,7 @@ describe('SplitTestHandler', function () {
     }
     this.AnalyticsManager = {
       getIdsFromSession: sinon.stub(),
-      setUserPropertyForAnalyticsId: sinon.stub(),
+      setUserPropertyForAnalyticsId: sinon.stub().resolves(),
     }
     this.LocalsHelper = {
       setSplitTestVariant: sinon.stub(),

+ 3 - 3
services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js

@@ -93,7 +93,7 @@ describe('FeaturesUpdater', function () {
       .resolves(this.user)
 
     this.AnalyticsManager = {
-      setUserPropertyForUser: sinon.stub(),
+      setUserPropertyForUserInBackground: sinon.stub(),
     }
     this.Modules = {
       promises: { hooks: { fire: sinon.stub().resolves() } },
@@ -141,7 +141,7 @@ describe('FeaturesUpdater', function () {
 
       it('should send the corresponding feature set user property', function () {
         expect(
-          this.AnalyticsManager.setUserPropertyForUser
+          this.AnalyticsManager.setUserPropertyForUserInBackground
         ).to.have.been.calledWith(this.user._id, 'feature-set', 'all')
       })
     })
@@ -159,7 +159,7 @@ describe('FeaturesUpdater', function () {
 
       it('should send mixed feature set user property', function () {
         sinon.assert.calledWith(
-          this.AnalyticsManager.setUserPropertyForUser,
+          this.AnalyticsManager.setUserPropertyForUserInBackground,
           this.user._id,
           'feature-set',
           'mixed'

+ 36 - 30
services/web/test/unit/src/Subscription/RecurlyEventHandlerTests.js

@@ -31,8 +31,8 @@ describe('RecurlyEventHandler', function () {
           sendTrialOnboardingEmail: sinon.stub(),
         }),
         '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
-          recordEventForUser: sinon.stub(),
-          setUserPropertyForUser: sinon.stub(),
+          recordEventForUserInBackground: sinon.stub(),
+          setUserPropertyForUserInBackground: sinon.stub(),
         }),
         '../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
           promises: {
@@ -51,7 +51,7 @@ describe('RecurlyEventHandler', function () {
       this.eventData
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-started',
       {
@@ -62,19 +62,19 @@ describe('RecurlyEventHandler', function () {
       }
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-plan-code',
       this.planCode
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-state',
       'active'
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-is-trial',
       true
@@ -104,7 +104,7 @@ describe('RecurlyEventHandler', function () {
       this.eventData
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-started',
       {
@@ -115,13 +115,13 @@ describe('RecurlyEventHandler', function () {
       }
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-state',
       'active'
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-is-trial',
       false
@@ -136,7 +136,7 @@ describe('RecurlyEventHandler', function () {
       this.eventData
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-updated',
       {
@@ -147,19 +147,19 @@ describe('RecurlyEventHandler', function () {
       }
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-plan-code',
       this.planCode
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-state',
       'active'
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-is-trial',
       true
@@ -173,7 +173,7 @@ describe('RecurlyEventHandler', function () {
       this.eventData
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-cancelled',
       {
@@ -184,13 +184,13 @@ describe('RecurlyEventHandler', function () {
       }
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-state',
       'cancelled'
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-is-trial',
       true
@@ -204,7 +204,7 @@ describe('RecurlyEventHandler', function () {
       this.eventData
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-expired',
       {
@@ -215,19 +215,19 @@ describe('RecurlyEventHandler', function () {
       }
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-plan-code',
       this.planCode
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-state',
       'expired'
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.setUserPropertyForUser,
+      this.AnalyticsManager.setUserPropertyForUserInBackground,
       this.userId,
       'subscription-is-trial',
       true
@@ -240,7 +240,7 @@ describe('RecurlyEventHandler', function () {
       this.eventData
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-renewed',
       {
@@ -258,7 +258,7 @@ describe('RecurlyEventHandler', function () {
       this.eventData
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-reactivated',
       {
@@ -292,7 +292,7 @@ describe('RecurlyEventHandler', function () {
       }
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-invoice-collected',
       {
@@ -321,7 +321,7 @@ describe('RecurlyEventHandler', function () {
         },
       }
     )
-    sinon.assert.notCalled(this.AnalyticsManager.recordEventForUser)
+    sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
   })
 
   it('with closed_invoice_notification', function () {
@@ -338,7 +338,7 @@ describe('RecurlyEventHandler', function () {
       }
     )
     sinon.assert.calledWith(
-      this.AnalyticsManager.recordEventForUser,
+      this.AnalyticsManager.recordEventForUserInBackground,
       this.userId,
       'subscription-invoice-collected'
     )
@@ -357,7 +357,7 @@ describe('RecurlyEventHandler', function () {
         },
       }
     )
-    sinon.assert.notCalled(this.AnalyticsManager.recordEventForUser)
+    sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
   })
 
   it('nothing is called with invalid account code', function () {
@@ -367,9 +367,15 @@ describe('RecurlyEventHandler', function () {
       'new_subscription_notification',
       this.eventData
     )
-    sinon.assert.notCalled(this.AnalyticsManager.recordEventForUser)
-    sinon.assert.notCalled(this.AnalyticsManager.setUserPropertyForUser)
-    sinon.assert.notCalled(this.AnalyticsManager.setUserPropertyForUser)
-    sinon.assert.notCalled(this.AnalyticsManager.setUserPropertyForUser)
+    sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground)
+    sinon.assert.notCalled(
+      this.AnalyticsManager.setUserPropertyForUserInBackground
+    )
+    sinon.assert.notCalled(
+      this.AnalyticsManager.setUserPropertyForUserInBackground
+    )
+    sinon.assert.notCalled(
+      this.AnalyticsManager.setUserPropertyForUserInBackground
+    )
   })
 })

+ 11 - 11
services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js

@@ -146,8 +146,8 @@ describe('SubscriptionUpdater', function () {
     }
 
     this.AnalyticsManager = {
-      recordEventForUser: sinon.stub().resolves(),
-      setUserPropertyForUser: sinon.stub(),
+      recordEventForUserInBackground: sinon.stub().resolves(),
+      setUserPropertyForUserInBackground: sinon.stub(),
     }
 
     this.Features = {
@@ -448,7 +448,7 @@ describe('SubscriptionUpdater', function () {
         .calledWith(searchOps, insertOperation)
         .should.equal(true)
       sinon.assert.calledWith(
-        this.AnalyticsManager.recordEventForUser,
+        this.AnalyticsManager.recordEventForUserInBackground,
         this.otherUserId,
         'group-subscription-joined',
         {
@@ -477,7 +477,7 @@ describe('SubscriptionUpdater', function () {
         this.otherUserId
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForUser,
+        this.AnalyticsManager.setUserPropertyForUserInBackground,
         this.otherUserId,
         'group-subscription-plan-code',
         'group_subscription'
@@ -493,7 +493,7 @@ describe('SubscriptionUpdater', function () {
         this.otherUserId
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForUser,
+        this.AnalyticsManager.setUserPropertyForUserInBackground,
         this.otherUserId,
         'group-subscription-plan-code',
         'better_group_subscription'
@@ -509,7 +509,7 @@ describe('SubscriptionUpdater', function () {
         this.otherUserId
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForUser,
+        this.AnalyticsManager.setUserPropertyForUserInBackground,
         this.otherUserId,
         'group-subscription-plan-code',
         'better_group_subscription'
@@ -567,7 +567,7 @@ describe('SubscriptionUpdater', function () {
         this.otherUserId
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.recordEventForUser,
+        this.AnalyticsManager.recordEventForUserInBackground,
         this.otherUserId,
         'group-subscription-left',
         {
@@ -583,7 +583,7 @@ describe('SubscriptionUpdater', function () {
         this.otherUserId
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForUser,
+        this.AnalyticsManager.setUserPropertyForUserInBackground,
         this.otherUserId,
         'group-subscription-plan-code',
         null
@@ -635,7 +635,7 @@ describe('SubscriptionUpdater', function () {
         this.otherUserId
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.setUserPropertyForUser,
+        this.AnalyticsManager.setUserPropertyForUserInBackground,
         this.otherUserId,
         'group-subscription-plan-code',
         null
@@ -682,7 +682,7 @@ describe('SubscriptionUpdater', function () {
         this.otherUserId
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.recordEventForUser,
+        this.AnalyticsManager.recordEventForUserInBackground,
         this.otherUserId,
         'group-subscription-left',
         {
@@ -691,7 +691,7 @@ describe('SubscriptionUpdater', function () {
         }
       )
       sinon.assert.calledWith(
-        this.AnalyticsManager.recordEventForUser,
+        this.AnalyticsManager.recordEventForUserInBackground,
         this.otherUserId,
         'group-subscription-left',
         {

+ 3 - 3
services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js

@@ -32,7 +32,7 @@ describe('TokenAccessHandler', function () {
         }),
         crypto: (this.Crypto = require('crypto')),
         '../Analytics/AnalyticsManager': (this.Analytics = {
-          recordEventForUser: sinon.stub(),
+          recordEventForUserInBackground: sinon.stub(),
         }),
       },
     })
@@ -127,7 +127,7 @@ describe('TokenAccessHandler', function () {
         'tokenAccessReadOnly_refs'
       )
       sinon.assert.calledWith(
-        this.Analytics.recordEventForUser,
+        this.Analytics.recordEventForUserInBackground,
         this.userId,
         'project-joined',
         { mode: 'read-only' }
@@ -175,7 +175,7 @@ describe('TokenAccessHandler', function () {
         'tokenAccessReadAndWrite_refs'
       )
       sinon.assert.calledWith(
-        this.Analytics.recordEventForUser,
+        this.Analytics.recordEventForUserInBackground,
         this.userId,
         'project-joined',
         { mode: 'read-write' }

+ 2 - 2
services/web/test/unit/src/User/ThirdPartyIdentityManagerTests.js

@@ -144,7 +144,7 @@ describe('ThirdPartyIdentityManager', function () {
 
       describe('EmailHandler', function () {
         beforeEach(function () {
-          this.EmailHandler.promises.sendEmail.throws(anError)
+          this.EmailHandler.promises.sendEmail.rejects(anError)
         })
         it('should log but not return the error', async function () {
           await expect(
@@ -219,7 +219,7 @@ describe('ThirdPartyIdentityManager', function () {
 
       describe('EmailHandler', function () {
         beforeEach(function () {
-          this.EmailHandler.promises.sendEmail.throws(anError)
+          this.EmailHandler.promises.sendEmail.rejects(anError)
         })
         it('should log but not return the error', async function () {
           await expect(

+ 2 - 2
services/web/test/unit/src/User/UserCreatorTests.js

@@ -43,7 +43,7 @@ describe('UserCreator', function () {
           },
         }),
         '../Analytics/AnalyticsManager': (this.Analytics = {
-          recordEventForUser: sinon.stub(),
+          recordEventForUserInBackground: sinon.stub(),
           setUserPropertyForUser: sinon.stub(),
         }),
         '../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
@@ -278,7 +278,7 @@ describe('UserCreator', function () {
         })
         assert.equal(user.email, this.email)
         sinon.assert.calledWith(
-          this.Analytics.recordEventForUser,
+          this.Analytics.recordEventForUserInBackground,
           user._id,
           'user-registered'
         )

+ 1 - 1
services/web/test/unit/src/User/UserEmailsControllerTests.js

@@ -57,7 +57,7 @@ describe('UserEmailsController', function () {
     }
     this.HttpErrorHandler = { conflict: sinon.stub() }
     this.AnalyticsManager = {
-      recordEventForUser: sinon.stub(),
+      recordEventForUserInBackground: sinon.stub(),
     }
     this.UserAuditLogHandler = {
       addEntry: sinon.stub().yields(),

+ 1 - 1
services/web/test/unit/src/User/UserRegistrationHandlerTests.js

@@ -40,7 +40,7 @@ describe('UserRegistrationHandler', function () {
       subscribe: sinon.stub(),
     }
     this.EmailHandler = {
-      promises: { sendEmail: sinon.stub() },
+      promises: { sendEmail: sinon.stub().resolves() },
     }
     this.OneTimeTokenHandler = { promises: { getNewToken: sinon.stub() } }
     this.handler = SandboxedModule.require(modulePath, {

+ 1 - 1
services/web/test/unit/src/User/UserUpdaterTests.js

@@ -65,7 +65,7 @@ describe('UserUpdater', function () {
       },
     }
     this.AnalyticsManager = {
-      recordEventForUser: sinon.stub(),
+      recordEventForUserInBackground: sinon.stub(),
     }
     this.InstitutionsAPI = {
       promises: {

Некоторые файлы не были показаны из-за большого количества измененных файлов