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) { function recordEventForSession(session, event, segmentation) {
const { analyticsId, userId } = getIdsFromSession(session) const { analyticsId, userId } = getIdsFromSession(session)
if (!analyticsId) { 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( async function setUserPropertyForAnalyticsId(
analyticsId, analyticsId,
propertyName, 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) { function updateEditingSession(userId, projectId, countryCode, segmentation) {
if (!userId) { if (!userId) {
return return
@ -310,8 +338,11 @@ module.exports = {
identifyUser, identifyUser,
recordEventForSession, recordEventForSession,
recordEventForUser, recordEventForUser,
recordEventForUserInBackground,
setUserPropertyForUser, setUserPropertyForUser,
setUserPropertyForUserInBackground,
setUserPropertyForSession, setUserPropertyForSession,
setUserPropertyForSessionInBackground,
setUserPropertyForAnalyticsId, setUserPropertyForAnalyticsId,
updateEditingSession, updateEditingSession,
getIdsFromSession, getIdsFromSession,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
const { callbackify } = require('util') const { callbackify } = require('util')
const Settings = require('@overleaf/settings') const Settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
const EmailBuilder = require('./EmailBuilder') const EmailBuilder = require('./EmailBuilder')
const EmailSender = require('./EmailSender') const EmailSender = require('./EmailSender')
const Queues = require('../../infrastructure/Queues') const Queues = require('../../infrastructure/Queues')
@ -30,5 +31,7 @@ function sendDeferredEmail(emailType, opts, delay) {
'deferred-emails', 'deferred-emails',
{ data: { emailType, opts } }, { data: { emailType, opts } },
delay 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) return LinkedFilesController.handleError(err, req, res, next)
} }
if (name.endsWith('.bib')) { if (name.endsWith('.bib')) {
AnalyticsManager.recordEventForUser(userId, 'linked-bib-file', { AnalyticsManager.recordEventForUserInBackground(
integration: provider, userId,
}) 'linked-bib-file',
{
integration: provider,
}
)
} }
return res.json({ new_file_id: newFileId }) return res.json({ new_file_id: newFileId })
} }

View file

@ -435,12 +435,16 @@ const ProjectController = {
SplitTestSessionHandler.sessionMaintenance(req, null, () => {}) SplitTestSessionHandler.sessionMaintenance(req, null, () => {})
cb(null, defaultSettingsForAnonymousUser(userId)) cb(null, defaultSettingsForAnonymousUser(userId))
} else { } else {
// Ignore spurious floating promises warning until we promisify
// eslint-disable-next-line @typescript-eslint/no-floating-promises
User.updateOne( User.updateOne(
{ _id: new ObjectId(userId) }, { _id: new ObjectId(userId) },
{ $set: { lastActive: new Date() } }, { $set: { lastActive: new Date() } },
{}, {},
() => {} () => {}
) )
// Ignore spurious floating promises warning until we promisify
// eslint-disable-next-line @typescript-eslint/no-floating-promises
User.findById( User.findById(
userId, userId,
'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram completedTutorials writefull', '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) metrics.inc(metricName)
if (userId) { if (userId) {
AnalyticsManager.recordEventForUser(userId, 'project-opened', { AnalyticsManager.recordEventForUserInBackground(
projectId: project._id, userId,
}) 'project-opened',
{
projectId: project._id,
}
)
} }
// should not be used in place of split tests query param overrides (?my-split-test-name=my-variant) // 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) Object.assign(segmentation, attributes.segmentation)
segmentation.projectId = project._id segmentation.projectId = project._id
if (isImport) { if (isImport) {
AnalyticsManager.recordEventForUser( AnalyticsManager.recordEventForUserInBackground(
ownerId, ownerId,
'project-imported', 'project-imported',
segmentation segmentation
) )
} else { } else {
AnalyticsManager.recordEventForUser( AnalyticsManager.recordEventForUserInBackground(
ownerId, ownerId,
'project-created', 'project-created',
segmentation segmentation
@ -72,7 +72,7 @@ async function createBlankProject(
async function createProjectFromSnippet(ownerId, projectName, docLines) { async function createProjectFromSnippet(ownerId, projectName, docLines) {
const project = await _createBlankProject(ownerId, projectName) const project = await _createBlankProject(ownerId, projectName)
AnalyticsManager.recordEventForUser(ownerId, 'project-created', { AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
projectId: project._id, projectId: project._id,
}) })
await _createRootDoc(project, ownerId, docLines) await _createRootDoc(project, ownerId, docLines)
@ -85,7 +85,7 @@ async function createBasicProject(ownerId, projectName) {
const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName) const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName)
await _createRootDoc(project, ownerId, docLines) await _createRootDoc(project, ownerId, docLines)
AnalyticsManager.recordEventForUser(ownerId, 'project-created', { AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
projectId: project._id, projectId: project._id,
}) })
@ -97,7 +97,7 @@ async function createExampleProject(ownerId, projectName) {
await _addExampleProjectFiles(ownerId, projectName, project) await _addExampleProjectFiles(ownerId, projectName, project)
AnalyticsManager.recordEventForUser(ownerId, 'project-created', { AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
projectId: project._id, projectId: project._id,
}) })

View file

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

View file

@ -314,7 +314,19 @@ async function _getAssignment(
if (sync === true) { if (sync === true) {
await _recordAssignment(assignmentData) await _recordAssignment(assignmentData)
} else { } 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 // 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( AnalyticsManager.setUserPropertyForAnalyticsId(
user?.analyticsId || analyticsId || userId, effectiveAnalyticsId,
`split-test-${splitTestName}-${versionNumber}`, `split-test-${splitTestName}-${versionNumber}`,
selectedVariantName 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) return _makeAssignment(splitTest, selectedVariantName, currentVersion)
} }

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ const {
addRequiredCleanupHandlerBeforeDrainingConnections, addRequiredCleanupHandlerBeforeDrainingConnections,
} = require('../../infrastructure/GracefulShutdown') } = require('../../infrastructure/GracefulShutdown')
const { callbackifyAll } = require('@overleaf/promise-utils') const { callbackifyAll } = require('@overleaf/promise-utils')
const logger = require('@overleaf/logger')
const SystemMessageManager = { const SystemMessageManager = {
getMessages() { getMessages() {
@ -22,9 +23,14 @@ const SystemMessageManager = {
await message.save() await message.save()
}, },
async refreshCache() { refreshCache() {
const messages = await this.getMessagesFromDB() this.getMessagesFromDB()
this._cachedMessages = messages .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) { async addReadOnlyUserToProject(userId, projectId) {
userId = new ObjectId(userId.toString()) userId = new ObjectId(userId.toString())
projectId = new ObjectId(projectId.toString()) projectId = new ObjectId(projectId.toString())
Analytics.recordEventForUser(userId, 'project-joined', { Analytics.recordEventForUserInBackground(userId, 'project-joined', {
mode: 'read-only', mode: 'read-only',
}) })
@ -171,7 +171,7 @@ const TokenAccessHandler = {
async addReadAndWriteUserToProject(userId, projectId) { async addReadAndWriteUserToProject(userId, projectId) {
userId = new ObjectId(userId.toString()) userId = new ObjectId(userId.toString())
projectId = new ObjectId(projectId.toString()) projectId = new ObjectId(projectId.toString())
Analytics.recordEventForUser(userId, 'project-joined', { Analytics.recordEventForUserInBackground(userId, 'project-joined', {
mode: 'read-write', mode: 'read-write',
}) })

View file

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

View file

@ -43,7 +43,11 @@ async function recordRegistrationEvent(user) {
if (user.thirdPartyIdentifiers && user.thirdPartyIdentifiers.length > 0) { if (user.thirdPartyIdentifiers && user.thirdPartyIdentifiers.length > 0) {
segmentation.provider = user.thirdPartyIdentifiers[0].providerId segmentation.provider = user.thirdPartyIdentifiers[0].providerId
} }
Analytics.recordEventForUser(user._id, 'user-registered', segmentation) Analytics.recordEventForUserInBackground(
user._id,
'user-registered',
segmentation
)
} catch (err) { } catch (err) {
logger.warn({ err }, 'there was an error recording `user-registered` event') 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 delete req.session.pendingSecondaryEmail
AnalyticsManager.recordEventForUser(user._id, 'email-verified', { AnalyticsManager.recordEventForUserInBackground(
provider: 'email', user._id,
verification_type: 'token', 'email-verified',
isPrimary: false, {
}) provider: 'email',
verification_type: 'token',
isPrimary: false,
}
)
const redirectUrl = const redirectUrl =
AuthenticationController.getRedirectFromSession(req) || '/project' AuthenticationController.getRedirectFromSession(req) || '/project'
@ -427,7 +431,7 @@ async function primaryEmailCheckPage(req, res) {
return res.redirect('/project') return res.redirect('/project')
} }
AnalyticsManager.recordEventForUser( AnalyticsManager.recordEventForUserInBackground(
userId, userId,
'primary-email-check-page-displayed' 'primary-email-check-page-displayed'
) )
@ -439,7 +443,10 @@ async function primaryEmailCheck(req, res) {
await UserUpdater.promises.updateUser(userId, { await UserUpdater.promises.updateUser(userId, {
$set: { lastPrimaryEmailCheck: new Date() }, $set: { lastPrimaryEmailCheck: new Date() },
}) })
AnalyticsManager.recordEventForUser(userId, 'primary-email-check-done') AnalyticsManager.recordEventForUserInBackground(
userId,
'primary-email-check-done'
)
AsyncFormHelper.redirect(req, res, '/project') AsyncFormHelper.redirect(req, res, '/project')
} }
@ -610,7 +617,7 @@ const UserEmailsController = {
) )
} }
const isPrimary = user?.email === userData.email const isPrimary = user?.email === userData.email
AnalyticsManager.recordEventForUser( AnalyticsManager.recordEventForUserInBackground(
userData.userId, userData.userId,
'email-verified', 'email-verified',
{ {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -144,7 +144,7 @@ describe('ThirdPartyIdentityManager', function () {
describe('EmailHandler', function () { describe('EmailHandler', function () {
beforeEach(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 () { it('should log but not return the error', async function () {
await expect( await expect(
@ -219,7 +219,7 @@ describe('ThirdPartyIdentityManager', function () {
describe('EmailHandler', function () { describe('EmailHandler', function () {
beforeEach(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 () { it('should log but not return the error', async function () {
await expect( await expect(

View file

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

View file

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

View file

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

View file

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