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

Add typescript-eslint rule: no-floating-promises

GitOrigin-RevId: 8c3decdff537c885f5bfeb5250b7805480bc6602
This commit is contained in:
Eric Mc Sween 2024-05-21 15:07:24 -04:00 committed by Copybot
parent 814b085b44
commit 876ee4d967
48 changed files with 1156 additions and 798 deletions

601
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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
services/web/.eslintrc.js Normal file
View file

@ -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',
},
},
],
}

View file

@ -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,

View file

@ -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]

View file

@ -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

View file

@ -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',

View file

@ -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 = {

View file

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

View file

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

View file

@ -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 }

View file

@ -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')
})
}

View file

@ -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 })
}

View file

@ -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)

View file

@ -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,
})

View file

@ -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 } },

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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,
})

View file

@ -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')
})
},
}

View file

@ -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',
})

View file

@ -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(

View file

@ -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')
}

View file

@ -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',
{

View file

@ -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 }
},

View file

@ -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

View file

@ -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, {

View file

@ -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 },
{

View file

@ -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",

View file

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

View file

@ -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'

View file

@ -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'
)

View file

@ -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

View file

@ -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(),

View file

@ -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',
{

View file

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

View file

@ -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': {

View file

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

View file

@ -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'

View file

@ -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
)
})
})

View file

@ -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',
{

View file

@ -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' }

View file

@ -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(

View file

@ -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'
)

View file

@ -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(),

View file

@ -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, {

View file

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