Compare commits

..
Author SHA1 Message Date
Miguel Serrano f0022392ac Merge pull request #21327 from overleaf/msm-optional-subnet-rate-limiter
[web] Add option to disable subnet rate limiting (+CE/SP Hotfix `5.2.1`)

GitOrigin-RevId: 78d60c9638cede729dd93c3c2421f55b34c0dbfe
2024-11-29 15:11:35 +01:00
5347 changed files with 475386 additions and 424071 deletions
-25
View File
@@ -1,25 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[Makefile]
indent_style = tab
[*.go]
indent_style = tab
[*.{pug,coffee}]
indent_style = tab
[*.{pug,patch}]
trim_trailing_whitespace = false
[Jenkinsfile]
insert_final_newline = false
max_line_length = off
+44
View File
@@ -0,0 +1,44 @@
<!--
Note: If you are using www.overleaf.com and have a problem,
or if you would like to request a new feature please contact
the support team at support@overleaf.com
This form should only be used to report bugs in the
Community Edition release of Overleaf.
-->
<!-- BUG REPORT TEMPLATE -->
## Steps to Reproduce
<!-- Describe the steps leading up to when / where you found the bug. -->
<!-- Screenshots may be helpful here. -->
1.
2.
3.
## Expected Behaviour
<!-- What should have happened when you completed the steps above? -->
## Observed Behaviour
<!-- What actually happened when you completed the steps above? -->
<!-- Screenshots may be helpful here. -->
## Context
<!-- How has this issue affected you? What were you trying to accomplish? -->
## Technical Info
<!-- Provide any technical details that may be applicable (or N/A if not applicable). -->
* URL:
* Browser Name and version:
* Operating System and version (desktop or mobile):
* Signed in as:
* Project and/or file:
## Analysis
<!--- Optionally, document investigation of / suggest a fix for the bug, e.g. 'comes from this line / commit' -->
-53
View File
@@ -1,53 +0,0 @@
---
name: Bug report
about: Report a bug
title: ''
labels: type:bug
assignees: ''
---
<!--
Note: If you are using www.overleaf.com and have a problem,
or if you would like to request a new feature please contact
the support team at support@overleaf.com
This form should only be used to report bugs in the
Community Edition release of Overleaf.
-->
<!-- BUG REPORT TEMPLATE -->
## Steps to Reproduce
<!-- Describe the steps leading up to when / where you found the bug. -->
<!-- Screenshots may be helpful here. -->
1.
2.
3.
## Expected Behaviour
<!-- What should have happened when you completed the steps above? -->
## Observed Behaviour
<!-- What actually happened when you completed the steps above? -->
<!-- Screenshots may be helpful here. -->
## Context
<!-- How has this issue affected you? What were you trying to accomplish? -->
## Technical Info
<!-- Provide any technical details that may be applicable (or N/A if not applicable). -->
* URL:
* Browser Name and version:
* Operating System and version (desktop or mobile):
* Signed in as:
* Project and/or file:
## Analysis
<!--- Optionally, document investigation of / suggest a fix for the bug, e.g. 'comes from this line / commit' -->
+1 -1
View File
@@ -8,4 +8,4 @@
## Contributor Agreement
- [ ] I confirm I have signed the [Contributor License Agreement](https://github.com/overleaf/overleaf/blob/main/CONTRIBUTING.md#contributor-license-agreement)
- [ ] I confirm I have signed the [Contributor License Agreement](https://github.com/overleaf/overleaf/blob/master/CONTRIBUTING.md#contributor-license-agreement)
+1 -3
View File
@@ -14,8 +14,6 @@ When submitting an issue please describe the issue as clearly as possible, inclu
reproduce the bug, which situations it appears in, what you expected to happen, and what actually happens.
If you can include a screenshot for front end issues that is very helpful.
**Note**: If you are using [www.overleaf.com](www.overleaf.com) and have a problem, or if you would like to request a new feature, please contact the Support team at support@overleaf.com. Raise an issue here only to report bugs in the Community Edition release of Overleaf.
Pull Requests
-------------
@@ -36,7 +34,7 @@ Please see [our security policy](https://github.com/overleaf/overleaf/security/p
Contributor License Agreement
-----------------------------
Before we can accept any contributions of code, we need you to agree to our
Before we can accept any contributions of code, we need you to agree to our
[Contributor License Agreement](https://docs.google.com/forms/d/e/1FAIpQLSef79XH3mb7yIiMzZw-yALEegS-wyFetvjTiNBfZvf_IHD2KA/viewform?usp=sf_link).
This is to ensure that you own the copyright of your contribution, and that you
agree to give us a license to use it in both the open source version, and the version
+31 -38
View File
@@ -6,58 +6,47 @@
<h4 align="center">An open-source online real-time collaborative LaTeX editor.</h4>
<p align="center">
<a href="https://github.com/yu-i-i/overleaf-cep/wiki">Wiki</a> •
<a href="https://github.com/overleaf/overleaf/wiki">Wiki</a> •
<a href="https://www.overleaf.com/for/enterprises">Server Pro</a> •
<a href="#contributing">Contributing</a> •
<a href="https://mailchi.mp/overleaf.com/community-edition-and-server-pro">Mailing List</a> •
<a href="#authors">Authors</a> •
<a href="#license">License</a>
</p>
<img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Extended Community Edition">
<img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Community Edition">
<p align="center">
Figure 1: A screenshot of a project being edited in Overleaf Extended Community Edition.
Figure 1: A screenshot of a project being edited in Overleaf Community Edition.
</p>
## Community Edition
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. Overleaf runs a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
## Extended Community Edition
The present "extended" version of Overleaf CE includes:
- Template Gallery
- Sandboxed Compiles with TeX Live image selection
- LDAP authentication
- SAML authentication
- OpenID Connect authentication
- Real-time track changes and comments
- Autocomplete of reference keys
- Symbol Palette
- "From External URL" feature
> [!CAUTION]
> Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles.
Therefore, in any environment where not all users can be fully trusted, it is strongly recommended to enable the Sandboxed Compiles feature available in the Extended Community Edition.
For more information on Sandbox Compiles check out Overleaf [documentation](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles).
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. We run a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
## Enterprise
If you want help installing and maintaining Overleaf in your lab or workplace, Overleaf offers an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises).
If you want help installing and maintaining Overleaf in your lab or workplace, we offer an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises). It also includes more features for security (SSO with LDAP or SAML), administration and collaboration (e.g. tracked changes). [Find out more!](https://www.overleaf.com/for/enterprises)
## Keeping up to date
Sign up to the [mailing list](https://mailchi.mp/overleaf.com/community-edition-and-server-pro) to get updates on Overleaf releases and development.
## Installation
Detailed installation instructions can be found in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
Configuration details and release history for the Extended Community Edition can be found on the [Extended CE Wiki Page](https://github.com/yu-i-i/overleaf-cep/wiki).
We have detailed installation instructions in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
## Upgrading
If you are upgrading from a previous version of Overleaf, please see the [Release Notes section on the Wiki](https://github.com/overleaf/overleaf/wiki#release-notes) for all of the versions between your current version and the version you are upgrading to.
## Overleaf Docker Image
This repo contains two dockerfiles, [`Dockerfile-base`](server-ce/Dockerfile-base), which builds the
`sharelatex/sharelatex-base:ext-ce` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the
`sharelatex/sharelatex:ext-ce` image.
`sharelatex/sharelatex-base` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the
`sharelatex/sharelatex` (or "community") image.
The Base image generally contains the basic dependencies like `wget`, plus `texlive`.
This is split out because it's a pretty heavy set of
The Base image generally contains the basic dependencies like `wget` and
`aspell`, plus `texlive`. We split this out because it's a pretty heavy set of
dependencies, and it's nice to not have to rebuild all of that every time.
The `sharelatex/sharelatex` image extends the base image and adds the actual Overleaf code
@@ -65,19 +54,23 @@ and services.
Use `make build-base` and `make build-community` from `server-ce/` to build these images.
The [Phusion base-image](https://github.com/phusion/baseimage-docker)
(which is extended by the `base` image) provides a VM-like container
We use the [Phusion base-image](https://github.com/phusion/baseimage-docker)
(which is extended by our `base` image) to provide us with a VM-like container
in which to run the Overleaf services. Baseimage uses the `runit` service
manager to manage services, and init scripts from the `server-ce/runit`
folder are added.
manager to manage services, and we add our init-scripts from the `server-ce/runit`
folder.
## Contributing
Please see the [CONTRIBUTING](CONTRIBUTING.md) file for information on contributing to the development of Overleaf.
## Authors
[The Overleaf Team](https://www.overleaf.com/about)
[yu-i-i](https://github.com/yu-i-i/overleaf-cep) — Extensions for CE unless otherwise noted
[The Overleaf Team](https://www.overleaf.com/about)
## License
The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the [`LICENSE`](LICENSE) file.
Copyright (c) Overleaf, 2014-2025.
Copyright (c) Overleaf, 2014-2024.
-3
View File
@@ -1,3 +0,0 @@
/* eslint-disable no-undef */
rs.initiate({ _id: 'overleaf', members: [{ _id: 0, host: 'mongo:27017' }] })
+7 -3
View File
@@ -11,6 +11,12 @@ bin/build
> [!NOTE]
> If Docker is running out of RAM while building the services in parallel, create a `.env` file in this directory containing `COMPOSE_PARALLEL_LIMIT=1`.
Next, initialize the database:
```shell
bin/init
```
Then start the services:
```shell
@@ -42,7 +48,7 @@ To do this, use the included `bin/dev` script:
bin/dev
```
This will start all services using `node --watch`, which will automatically monitor the code and restart the services as necessary.
This will start all services using `nodemon`, which will automatically monitor the code and restart the services as necessary.
To improve performance, you can start only a subset of the services in development mode by providing a space-separated list to the `bin/dev` script:
@@ -77,10 +83,8 @@ each service:
| `filestore` | 9235 |
| `notifications` | 9236 |
| `real-time` | 9237 |
| `references` | 9238 |
| `history-v1` | 9239 |
| `project-history` | 9240 |
| `linked-url-proxy` | 9241 |
To attach to a service using Chrome's _remote debugging_, go to
<chrome://inspect/> and make sure _Discover network targets_ is checked. Next
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
docker compose up --detach mongo
curl --max-time 10 --retry 5 --retry-delay 5 --retry-all-errors --silent --output /dev/null localhost:27017
docker compose exec mongo mongosh --eval "rs.initiate({ _id: 'overleaf', members: [{ _id: 0, host: 'mongo:27017' }] })"
docker compose down mongo
+1 -7
View File
@@ -6,20 +6,14 @@ DOCUMENT_UPDATER_HOST=document-updater
FILESTORE_HOST=filestore
GRACEFUL_SHUTDOWN_DELAY_SECONDS=0
HISTORY_V1_HOST=history-v1
HISTORY_REDIS_HOST=redis
LINKED_URL_PROXY_HOST=linked-url-proxy
LISTEN_ADDRESS=0.0.0.0
MONGO_HOST=mongo
MONGO_URL=mongodb://mongo/sharelatex?directConnection=true
NOTIFICATIONS_HOST=notifications
PROJECT_HISTORY_HOST=project-history
QUEUES_REDIS_HOST=redis
DSMP_REDIS_HOST=redis
REALTIME_HOST=real-time
REDIS_HOST=redis
REFERENCES_HOST=references
SESSION_SECRET=foo
V1_HISTORY_HOST=history-v1
SPELLING_HOST=spelling
WEBPACK_HOST=webpack
WEB_API_PASSWORD=overleaf
WEB_API_USER=overleaf
+3 -25
View File
@@ -79,26 +79,15 @@ services:
- ../services/history-v1/knexfile.js:/overleaf/services/history-v1/knexfile.js
- ../services/history-v1/migrations:/overleaf/services/history-v1/migrations
linked-url-proxy:
command: ["node", "--watch", "app.mjs"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9241:9229"
volumes:
- ../services/linked-url-proxy/app:/overleaf/services/linked-url-proxy/app
- ../services/linked-url-proxy/config:/overleaf/services/linked-url-proxy/config
- ../services/linked-url-proxy/app.js:/overleaf/services/linked-url-proxy/app.js
notifications:
command: ["node", "--watch", "app.ts"]
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9236:9229"
volumes:
- ../services/notifications/app:/overleaf/services/notifications/app
- ../services/notifications/app.ts:/overleaf/services/notifications/app.ts
- ../services/notifications/app.js:/overleaf/services/notifications/app.js
- ../services/notifications/config:/overleaf/services/notifications/config
project-history:
@@ -123,19 +112,8 @@ services:
- ../services/real-time/app.js:/overleaf/services/real-time/app.js
- ../services/real-time/config:/overleaf/services/real-time/config
references:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9238:9229"
volumes:
- ../services/references/app:/overleaf/services/references/app
- ../services/references/config:/overleaf/services/references/config
- ../services/references/app.js:/overleaf/services/references/app.js
web:
command: ["node", "--watch", "app.mjs", "--watch-locales"]
command: ["node", "--watch", "app.js", "--watch-locales"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
+15 -27
View File
@@ -1,5 +1,6 @@
volumes:
clsi-cache:
clsi-output:
filestore-public-files:
filestore-template-files:
filestore-uploads:
@@ -7,6 +8,7 @@ volumes:
mongo-data:
redis-data:
sharelatex-data:
spelling-cache:
web-data:
history-v1-buckets:
@@ -25,16 +27,15 @@ services:
env_file:
- dev.env
environment:
- DOCKER_RUNNER=true
- TEXLIVE_IMAGE=texlive-full # docker build texlive -t texlive-full
- SANDBOXED_COMPILES=true
- SANDBOXED_COMPILES_HOST_DIR_COMPILES=${PWD}/compiles
- SANDBOXED_COMPILES_HOST_DIR_OUTPUT=${PWD}/output
- COMPILES_HOST_DIR=${PWD}/compiles
user: root
volumes:
- ${PWD}/compiles:/overleaf/services/clsi/compiles
- ${PWD}/output:/overleaf/services/clsi/output
- ${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock
- clsi-cache:/overleaf/services/clsi/cache
- clsi-output:/overleaf/services/clsi/output
contacts:
build:
@@ -69,6 +70,7 @@ services:
- filestore-public-files:/overleaf/services/filestore/public_files
- filestore-template-files:/overleaf/services/filestore/template_files
- filestore-uploads:/overleaf/services/filestore/uploads
- filestore-user-files:/overleaf/services/filestore/user_files
history-v1:
build:
@@ -86,28 +88,13 @@ services:
volumes:
- history-v1-buckets:/buckets
linked-url-proxy:
build:
context: ..
dockerfile: services/linked-url-proxy/Dockerfile
env_file:
- dev.env
mongo:
image: mongo:6.0
image: mongo:5
command: --replSet overleaf
ports:
- "127.0.0.1:27017:27017" # for debugging
volumes:
- mongo-data:/data/db
- ../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js
environment:
MONGO_INITDB_DATABASE: sharelatex
extra_hosts:
# Required when using the automatic database setup for initializing the
# replica set. This override is not needed when running the setup after
# starting up mongo.
- mongo:127.0.0.1
notifications:
build:
@@ -129,7 +116,7 @@ services:
dockerfile: services/real-time/Dockerfile
env_file:
- dev.env
redis:
image: redis:5
ports:
@@ -137,12 +124,14 @@ services:
volumes:
- redis-data:/data
references:
spelling:
build:
context: ..
dockerfile: services/references/Dockerfile
dockerfile: services/spelling/Dockerfile
env_file:
- dev.env
volumes:
- spelling-cache:/overleaf/services/spelling/cache
web:
build:
@@ -157,7 +146,7 @@ services:
- EMAIL_CONFIRMATION_DISABLED=true
- NODE_ENV=development
- OVERLEAF_ALLOW_PUBLIC_ACCESS=true
command: ["node", "app.mjs"]
command: ["node", "app.js"]
volumes:
- sharelatex-data:/var/lib/overleaf
- web-data:/overleaf/services/web/data
@@ -171,17 +160,16 @@ services:
- document-updater
- filestore
- history-v1
- linked-url-proxy
- notifications
- project-history
- real-time
- references
- spelling
webpack:
build:
context: ..
dockerfile: services/web/Dockerfile
target: webpack
target: dev
command: ["npx", "webpack", "serve", "--config", "webpack.config.dev-env.js"]
ports:
- "127.0.0.1:80:3808"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 587 KiB

+11 -14
View File
@@ -1,3 +1,4 @@
version: '2.2'
services:
sharelatex:
restart: always
@@ -39,6 +40,10 @@ services:
# Disables email confirmation requirement
EMAIL_CONFIRMATION_DISABLED: 'true'
# temporary fix for LuaLaTex compiles
# see https://github.com/overleaf/overleaf/issues/695
TEXMFVAR: /var/lib/overleaf/tmp/texmf-var
## Set for SSL via nginx-proxy
#VIRTUAL_HOST: 103.112.212.22
@@ -72,19 +77,11 @@ services:
## Server Pro ##
################
## The Community Edition is intended for use in environments where all users are trusted and is not appropriate for
## scenarios where isolation of users is required. Sandboxed Compiles are not available in the Community Edition,
## so the following environment variables must be commented out to avoid compile issues.
##
## Sandboxed Compiles: https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles
## Sandboxed Compiles: https://github.com/overleaf/overleaf/wiki/Server-Pro:-Sandboxed-Compiles
SANDBOXED_COMPILES: 'true'
### Bind-mount source for /var/lib/overleaf/data/compiles inside the container.
SANDBOXED_COMPILES_HOST_DIR_COMPILES: '/home/user/sharelatex_data/data/compiles'
### Bind-mount source for /var/lib/overleaf/data/output inside the container.
SANDBOXED_COMPILES_HOST_DIR_OUTPUT: '/home/user/sharelatex_data/data/output'
### Backwards compatibility (before Server Pro 5.5)
DOCKER_RUNNER: 'true'
SANDBOXED_COMPILES_SIBLING_CONTAINERS: 'true'
### Bind-mount source for /var/lib/overleaf/data/compiles inside the container.
SANDBOXED_COMPILES_HOST_DIR: '/home/user/sharelatex_data/data/compiles'
## Works with test LDAP server shown at bottom of docker compose
# OVERLEAF_LDAP_URL: 'ldap://ldap:389'
@@ -105,12 +102,12 @@ services:
mongo:
restart: always
image: mongo:6.0
image: mongo:5.0
container_name: mongo
command: '--replSet overleaf'
volumes:
- ~/mongo_data:/data/db
- ./bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js
- ./mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js
environment:
MONGO_INITDB_DATABASE: sharelatex
extra_hosts:
@@ -118,7 +115,7 @@ services:
# This override is not needed when running the setup after starting up mongo.
- mongo:127.0.0.1
healthcheck:
test: echo 'db.stats().ok' | mongosh localhost:27017/test --quiet
test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
@@ -0,0 +1 @@
node_modules/
@@ -0,0 +1,46 @@
compileFolder
Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db
/node_modules/*
data/*/*
**.swp
/log.json
hash_folder
.npmrc
@@ -1,7 +0,0 @@
# Autogenerated by build scripts. Do not edit.
.eslint*
.prettier*
libraries/access-token-encryptor/**
package-lock.json
package.json
patches/**
@@ -1,13 +0,0 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
const all = {
require: 'test/setup.js',
...reporterOptions,
}
module.exports = all
@@ -0,0 +1,6 @@
{
"ui": "bdd",
"recursive": "true",
"reporter": "spec",
"require": "test/setup.js"
}
+1 -1
View File
@@ -1 +1 @@
22.18.0
18.20.2
-82
View File
@@ -1,82 +0,0 @@
// Autogenerated by build scripts. Do not edit.
pipeline {
agent {
node {
label 'jenkins-agent-web'
customWorkspace '/workspace'
}
}
options {
timestamps()
timeout(time: 15, unit: 'MINUTES')
}
environment {
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
COMMIT_SHA = "${GIT_COMMIT}"
SHORT_SHA = "${GIT_COMMIT.take(7)}"
}
stages {
stage('Stage 1') {
parallel {
stage('Install monorepo') {
steps {
retry(count: 3) {
sh 'make monorepo_setup'
}
}
}
stage('Create reports folder') {
steps {
sh 'mkdir libraries/access-token-encryptor/reports'
}
}
}
}
stage('Stage 2') {
parallel {
stage('Lint') {
steps {
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run lint -- --format json --output-file reports/eslint.json'
}
post {
always {
sh """
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/access-token-encryptor/reports/eslint.json
"""
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'access-token-encryptor-eslint', name: 'access-token-encryptor eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/access-token-encryptor/reports/eslint.json')]
}
}
}
stage('Format') {
steps {
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run format'
}
}
stage('Typecheck') {
steps {
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run types:check'
}
}
stage('Test') {
steps {
retry(count: 3) {
sh 'bin/run -w /overleaf/libraries/access-token-encryptor monorepo npm run test:ci'
}
}
}
}
}
}
post {
always {
junit checksName: 'access-token-encryptor test results', testResults: 'libraries/access-token-encryptor/reports/junit-*.xml'
}
cleanup {
sh 'rm -rf libraries/access-token-encryptor/reports'
sh 'make clean_jenkins -j10'
}
}
}
@@ -1,8 +1,10 @@
access-token-encryptor
--dependencies=None
--docker-repos=gcr.io/overleaf-ops
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.18.0
--node-version=18.20.2
--public-repo=False
--script-version=4.5.0
@@ -1,5 +1,5 @@
const { promisify } = require('node:util')
const crypto = require('node:crypto')
const { promisify } = require('util')
const crypto = require('crypto')
const ALGORITHM = 'aes-256-ctr'
@@ -5,8 +5,8 @@
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"lint": "eslint --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
"test:ci": "npm run test:unit",
@@ -21,9 +21,7 @@
"devDependencies": {
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"mocha": "^10.2.0",
"sandboxed-module": "^2.0.4",
"typescript": "^5.0.4"
}
@@ -1,8 +0,0 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
includePending: true,
jenkinsMode: true,
},
}
@@ -1,13 +1,4 @@
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const SandboxedModule = require('sandboxed-module')
chai.use(chaiAsPromised)
SandboxedModule.configure({
sourceTransformers: {
removeNodePrefix: function (source) {
return source.replace(/require\(['"]node:/g, "require('")
},
},
})
@@ -2,7 +2,6 @@
"extends": "../../tsconfig.backend.json",
"include": [
"**/*.js",
"**/*.cjs",
"**/*.ts"
"**/*.cjs"
]
}
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
# managed by monorepo$ bin/update_build_scripts
.npmrc
@@ -1,8 +0,0 @@
# Autogenerated by build scripts. Do not edit.
.eslint*
.prettier*
libraries/fetch-utils/**
libraries/o-error/**
package-lock.json
package.json
patches/**
-13
View File
@@ -1,13 +0,0 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
const all = {
require: 'test/setup.js',
...reporterOptions,
}
module.exports = all
+6
View File
@@ -0,0 +1,6 @@
{
"ui": "bdd",
"recursive": "true",
"reporter": "spec",
"require": "test/setup.js"
}
+1 -1
View File
@@ -1 +1 @@
22.18.0
18.20.2
-82
View File
@@ -1,82 +0,0 @@
// Autogenerated by build scripts. Do not edit.
pipeline {
agent {
node {
label 'jenkins-agent-web'
customWorkspace '/workspace'
}
}
options {
timestamps()
timeout(time: 15, unit: 'MINUTES')
}
environment {
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
COMMIT_SHA = "${GIT_COMMIT}"
SHORT_SHA = "${GIT_COMMIT.take(7)}"
}
stages {
stage('Stage 1') {
parallel {
stage('Install monorepo') {
steps {
retry(count: 3) {
sh 'make monorepo_setup'
}
}
}
stage('Create reports folder') {
steps {
sh 'mkdir libraries/fetch-utils/reports'
}
}
}
}
stage('Stage 2') {
parallel {
stage('Lint') {
steps {
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run lint -- --format json --output-file reports/eslint.json'
}
post {
always {
sh """
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/fetch-utils/reports/eslint.json
"""
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'fetch-utils-eslint', name: 'fetch-utils eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/fetch-utils/reports/eslint.json')]
}
}
}
stage('Format') {
steps {
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run format'
}
}
stage('Typecheck') {
steps {
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run types:check'
}
}
stage('Test') {
steps {
retry(count: 3) {
sh 'bin/run -w /overleaf/libraries/fetch-utils monorepo npm run test:ci'
}
}
}
}
}
}
post {
always {
junit checksName: 'fetch-utils test results', testResults: 'libraries/fetch-utils/reports/junit-*.xml'
}
cleanup {
sh 'rm -rf libraries/fetch-utils/reports'
sh 'make clean_jenkins -j10'
}
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
fetch-utils
--dependencies=None
--docker-repos=gcr.io/overleaf-ops
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.18.0
--node-version=18.20.2
--public-repo=False
--script-version=4.5.0
+17 -37
View File
@@ -1,9 +1,9 @@
const _ = require('lodash')
const { Readable } = require('node:stream')
const { Readable } = require('stream')
const OError = require('@overleaf/o-error')
const fetch = require('node-fetch')
const http = require('node:http')
const https = require('node:https')
const http = require('http')
const https = require('https')
/**
* @import { Response } from 'node-fetch'
@@ -23,11 +23,11 @@ async function fetchJson(url, opts = {}) {
}
async function fetchJsonWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts } = parseOpts(opts)
fetchOpts.headers = fetchOpts.headers ?? {}
fetchOpts.headers.Accept = fetchOpts.headers.Accept ?? 'application/json'
const response = await performRequest(url, fetchOpts, detachSignal)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -53,8 +53,8 @@ async function fetchStream(url, opts = {}) {
}
async function fetchStreamWithResponse(url, opts = {}) {
const { fetchOpts, abortController, detachSignal } = parseOpts(opts)
const response = await performRequest(url, fetchOpts, detachSignal)
const { fetchOpts, abortController } = parseOpts(opts)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
@@ -76,8 +76,8 @@ async function fetchStreamWithResponse(url, opts = {}) {
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchNothing(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const response = await performRequest(url, fetchOpts, detachSignal)
const { fetchOpts } = parseOpts(opts)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -95,22 +95,9 @@ async function fetchNothing(url, opts = {}) {
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirect(url, opts = {}) {
const { location } = await fetchRedirectWithResponse(url, opts)
return location
}
/**
* Make a request and extract the redirect from the response.
*
* @param {string | URL} url - request URL
* @param {object} opts - fetch options
* @return {Promise<{location: string, response: Response}>}
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirectWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts } = parseOpts(opts)
fetchOpts.redirect = 'manual'
const response = await performRequest(url, fetchOpts, detachSignal)
const response = await performRequest(url, fetchOpts)
if (response.status < 300 || response.status >= 400) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -125,7 +112,7 @@ async function fetchRedirectWithResponse(url, opts = {}) {
)
}
await discardResponseBody(response)
return { location, response }
return location
}
/**
@@ -142,8 +129,8 @@ async function fetchString(url, opts = {}) {
}
async function fetchStringWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const response = await performRequest(url, fetchOpts, detachSignal)
const { fetchOpts } = parseOpts(opts)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -178,14 +165,13 @@ function parseOpts(opts) {
const abortController = new AbortController()
fetchOpts.signal = abortController.signal
let detachSignal = () => {}
if (opts.signal) {
detachSignal = abortOnSignal(abortController, opts.signal)
abortOnSignal(abortController, opts.signal)
}
if (opts.body instanceof Readable) {
abortOnDestroyedRequest(abortController, fetchOpts.body)
}
return { fetchOpts, abortController, detachSignal }
return { fetchOpts, abortController }
}
function setupJsonBody(fetchOpts, json) {
@@ -209,9 +195,6 @@ function abortOnSignal(abortController, signal) {
abortController.abort(signal.reason)
}
signal.addEventListener('abort', listener)
return () => {
signal.removeEventListener('abort', listener)
}
}
function abortOnDestroyedRequest(abortController, stream) {
@@ -230,12 +213,11 @@ function abortOnDestroyedResponse(abortController, response) {
})
}
async function performRequest(url, fetchOpts, detachSignal) {
async function performRequest(url, fetchOpts) {
let response
try {
response = await fetch(url, fetchOpts)
} catch (err) {
detachSignal()
if (fetchOpts.body instanceof Readable) {
fetchOpts.body.destroy()
}
@@ -244,7 +226,6 @@ async function performRequest(url, fetchOpts, detachSignal) {
method: fetchOpts.method ?? 'GET',
})
}
response.body.on('close', detachSignal)
if (fetchOpts.body instanceof Readable) {
response.body.on('close', () => {
if (!fetchOpts.body.readableEnded) {
@@ -316,7 +297,6 @@ module.exports = {
fetchStreamWithResponse,
fetchNothing,
fetchRedirect,
fetchRedirectWithResponse,
fetchString,
fetchStringWithResponse,
RequestFailedError,
+5 -7
View File
@@ -5,8 +5,8 @@
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"lint": "eslint --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
"test:ci": "npm run test:unit",
@@ -16,14 +16,12 @@
"author": "Overleaf (https://www.overleaf.com)",
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/node-fetch": "^2.6.13",
"@types/node-fetch": "^2.6.11",
"body-parser": "^1.20.3",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"express": "^4.21.2",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"express": "^4.21.0",
"mocha": "^10.2.0",
"typescript": "^5.0.4"
},
"dependencies": {
@@ -1,8 +0,0 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
includePending: true,
jenkinsMode: true,
},
}
@@ -1,10 +1,7 @@
const { expect } = require('chai')
const fs = require('node:fs')
const events = require('node:events')
const { FetchError, AbortError } = require('node-fetch')
const { Readable } = require('node:stream')
const { pipeline } = require('node:stream/promises')
const { once } = require('node:events')
const { Readable } = require('stream')
const { once } = require('events')
const { TestServer } = require('./helpers/TestServer')
const selfsigned = require('selfsigned')
const {
@@ -27,17 +24,13 @@ const pems = selfsigned.generate(attrs, { days: 365 })
const PRIVATE_KEY = pems.private
const PUBLIC_CERT = pems.cert
const dns = require('node:dns')
const dns = require('dns')
const _originalLookup = dns.lookup
// Custom DNS resolver function
dns.lookup = (hostname, options, callback) => {
if (hostname === 'example.com') {
// If the hostname is our test case, return the ip address for the test server
if (options?.all) {
callback(null, [{ address: '127.0.0.1', family: 4 }])
} else {
callback(null, '127.0.0.1', 4)
}
callback(null, '127.0.0.1', 4)
} else {
// Otherwise, use the default lookup
_originalLookup(hostname, options, callback)
@@ -206,31 +199,6 @@ describe('fetch-utils', function () {
).to.be.rejectedWith(AbortError)
expect(stream.destroyed).to.be.true
})
it('detaches from signal on success', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
const s = await fetchStream(this.url('/hello'), { signal })
expect(events.getEventListeners(signal, 'abort')).to.have.length(1)
await pipeline(s, fs.createWriteStream('/dev/null'))
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
})
it('detaches from signal on error', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
try {
await fetchStream(this.url('/500'), { signal })
} catch (err) {
if (err instanceof RequestFailedError && err.response.status === 500)
continue
throw err
} finally {
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
}
})
})
describe('fetchNothing', function () {
@@ -419,16 +387,9 @@ async function* infiniteIterator() {
async function abortOnceReceived(func, server) {
const controller = new AbortController()
const promise = func(controller.signal)
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(1)
await once(server.events, 'request-received')
controller.abort()
try {
return await promise
} finally {
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(
0
)
}
return await promise
}
async function expectRequestAborted(req) {
@@ -1,9 +1,9 @@
const express = require('express')
const bodyParser = require('body-parser')
const { EventEmitter } = require('node:events')
const http = require('node:http')
const https = require('node:https')
const { promisify } = require('node:util')
const { EventEmitter } = require('events')
const http = require('http')
const https = require('https')
const { promisify } = require('util')
class TestServer {
constructor() {
+1 -2
View File
@@ -2,7 +2,6 @@
"extends": "../../tsconfig.backend.json",
"include": [
"**/*.js",
"**/*.cjs",
"**/*.ts"
"**/*.cjs"
]
}
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
node_modules
.npmrc
-9
View File
@@ -1,9 +0,0 @@
# Autogenerated by build scripts. Do not edit.
.eslint*
.prettier*
libraries/fetch-utils/**
libraries/logger/**
libraries/o-error/**
package-lock.json
package.json
patches/**
-13
View File
@@ -1,13 +0,0 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
const all = {
require: 'test/setup.js',
...reporterOptions,
}
module.exports = all
+3
View File
@@ -0,0 +1,3 @@
{
"require": "test/setup.js"
}
+1 -1
View File
@@ -1 +1 @@
22.18.0
18.20.2
-82
View File
@@ -1,82 +0,0 @@
// Autogenerated by build scripts. Do not edit.
pipeline {
agent {
node {
label 'jenkins-agent-web'
customWorkspace '/workspace'
}
}
options {
timestamps()
timeout(time: 15, unit: 'MINUTES')
}
environment {
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
COMMIT_SHA = "${GIT_COMMIT}"
SHORT_SHA = "${GIT_COMMIT.take(7)}"
}
stages {
stage('Stage 1') {
parallel {
stage('Install monorepo') {
steps {
retry(count: 3) {
sh 'make monorepo_setup'
}
}
}
stage('Create reports folder') {
steps {
sh 'mkdir libraries/logger/reports'
}
}
}
}
stage('Stage 2') {
parallel {
stage('Lint') {
steps {
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run lint -- --format json --output-file reports/eslint.json'
}
post {
always {
sh """
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/logger/reports/eslint.json
"""
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'logger-eslint', name: 'logger eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/logger/reports/eslint.json')]
}
}
}
stage('Format') {
steps {
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run format'
}
}
stage('Typecheck') {
steps {
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run types:check'
}
}
stage('Test') {
steps {
retry(count: 3) {
sh 'bin/run -w /overleaf/libraries/logger monorepo npm run test:ci'
}
}
}
}
}
}
post {
always {
junit checksName: 'logger test results', testResults: 'libraries/logger/reports/junit-*.xml'
}
cleanup {
sh 'rm -rf libraries/logger/reports'
sh 'make clean_jenkins -j10'
}
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
logger
--dependencies=None
--docker-repos=gcr.io/overleaf-ops
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.18.0
--node-version=18.20.2
--public-repo=False
--script-version=4.5.0
+1 -1
View File
@@ -1,5 +1,5 @@
const { fetchString } = require('@overleaf/fetch-utils')
const fs = require('node:fs')
const fs = require('fs')
class LogLevelChecker {
constructor(logger, defaultLevel) {
+15 -6
View File
@@ -1,6 +1,7 @@
const Stream = require('node:stream')
const Stream = require('stream')
const bunyan = require('bunyan')
const GCPManager = require('./gcp-manager')
const SentryManager = require('./sentry-manager')
const Serializers = require('./serializers')
const {
FileLogLevelChecker,
@@ -11,13 +12,11 @@ const LoggingManager = {
/**
* @param {string} name - The name of the logger
*/
initialize(name, options = {}) {
initialize(name) {
this.isProduction =
(process.env.NODE_ENV || '').toLowerCase() === 'production'
const isTest = (process.env.NODE_ENV || '').toLowerCase() === 'test'
this.defaultLevel =
process.env.LOG_LEVEL ||
(this.isProduction ? 'info' : isTest ? 'fatal' : 'debug')
process.env.LOG_LEVEL || (this.isProduction ? 'info' : 'debug')
this.loggerName = name
this.logger = bunyan.createLogger({
name,
@@ -27,13 +26,17 @@ const LoggingManager = {
req: Serializers.req,
res: Serializers.res,
},
streams: options.streams ?? [this._getOutputStreamConfig()],
streams: [this._getOutputStreamConfig()],
})
this._setupRingBuffer()
this._setupLogLevelChecker()
return this
},
initializeErrorReporting(dsn, options) {
this.sentryManager = new SentryManager()
},
/**
* @param {Record<string, any>|string} attributes - Attributes to log (nice serialization for err, req, res)
* @param {string} [message] - Optional message
@@ -65,6 +68,9 @@ const LoggingManager = {
})
}
this.logger.error(attributes, message, ...Array.from(args))
if (this.sentryManager) {
this.sentryManager.captureExceptionRateLimited(attributes, message)
}
},
/**
@@ -92,6 +98,9 @@ const LoggingManager = {
*/
fatal(attributes, message) {
this.logger.fatal(attributes, message)
if (this.sentryManager) {
this.sentryManager.captureException(attributes, message, 'fatal')
}
},
_getOutputStreamConfig() {
+4 -5
View File
@@ -13,8 +13,8 @@
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
"lint": "eslint --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
"test:ci": "npm run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
@@ -23,13 +23,12 @@
"@google-cloud/logging-bunyan": "^5.1.0",
"@overleaf/fetch-utils": "*",
"@overleaf/o-error": "*",
"@sentry/node": "^6.13.2",
"bunyan": "^1.8.14"
},
"devDependencies": {
"chai": "^4.3.6",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"mocha": "^10.2.0",
"sandboxed-module": "^2.0.4",
"sinon": "^9.2.4",
"sinon-chai": "^3.7.0",
+106
View File
@@ -0,0 +1,106 @@
const Serializers = require('./serializers')
const RATE_LIMIT_MAX_ERRORS = 5
const RATE_LIMIT_INTERVAL_MS = 60000
class SentryManager {
constructor(dsn, options) {
this.Sentry = require('@sentry/node')
this.Sentry.init({ dsn, ...options })
// for rate limiting on sentry reporting
this.lastErrorTimeStamp = 0
this.lastErrorCount = 0
}
captureExceptionRateLimited(attributes, message) {
const now = Date.now()
// have we recently reported an error?
const recentSentryReport =
now - this.lastErrorTimeStamp < RATE_LIMIT_INTERVAL_MS
// if so, increment the error count
if (recentSentryReport) {
this.lastErrorCount++
} else {
this.lastErrorCount = 0
this.lastErrorTimeStamp = now
}
// only report 5 errors every minute to avoid overload
if (this.lastErrorCount < RATE_LIMIT_MAX_ERRORS) {
// add a note if the rate limit has been hit
const note =
this.lastErrorCount + 1 === RATE_LIMIT_MAX_ERRORS
? '(rate limited)'
: ''
// report the exception
this.captureException(attributes, message, `error${note}`)
}
}
captureException(attributes, message, level) {
// handle case of logger.error "message"
if (typeof attributes === 'string') {
attributes = { err: new Error(attributes) }
}
// extract any error object
let error = Serializers.err(attributes.err || attributes.error)
// avoid reporting errors twice
for (const key in attributes) {
const value = attributes[key]
if (value instanceof Error && value.reportedToSentry) {
return
}
}
// include our log message in the error report
if (error == null) {
if (typeof message === 'string') {
error = { message }
}
} else if (message != null) {
attributes.description = message
}
// report the error
if (error != null) {
// capture attributes and use *_id objects as tags
const tags = {}
const extra = {}
for (const key in attributes) {
let value = attributes[key]
if (Serializers[key]) {
value = Serializers[key](value)
}
if (key.match(/_id/) && typeof value === 'string') {
tags[key] = value
}
extra[key] = value
}
// OError integration
extra.info = error.info
delete error.info
// Sentry wants to receive an Error instance.
const errInstance = new Error(error.message)
Object.assign(errInstance, error)
try {
// send the error to sentry
this.Sentry.captureException(errInstance, { tags, extra, level })
// put a flag on the errors to avoid reporting them multiple times
for (const key in attributes) {
const value = attributes[key]
if (value instanceof Error) {
value.reportedToSentry = true
}
}
} catch (err) {
// ignore Sentry errors
}
}
}
}
module.exports = SentryManager
@@ -1,8 +0,0 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
includePending: true,
jenkinsMode: true,
},
}
-5
View File
@@ -8,9 +8,4 @@ chai.use(sinonChai)
SandboxedModule.configure({
globals: { Buffer, JSON, console, process },
sourceTransformers: {
removeNodePrefix: function (source) {
return source.replace(/require\(['"]node:/g, "require('")
},
},
})
@@ -1,5 +1,5 @@
const Path = require('node:path')
const { promisify } = require('node:util')
const Path = require('path')
const { promisify } = require('util')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
@@ -1,7 +1,7 @@
const SandboxedModule = require('sandboxed-module')
const bunyan = require('bunyan')
const { expect } = require('chai')
const path = require('node:path')
const path = require('path')
const sinon = require('sinon')
const MODULE_PATH = path.join(__dirname, '../../logging-manager.js')
@@ -43,14 +43,20 @@ describe('LoggingManager', function () {
.stub()
.returns(this.GCEMetadataLogLevelChecker),
}
this.SentryManager = {
captureException: sinon.stub(),
captureExceptionRateLimited: sinon.stub(),
}
this.LoggingManager = SandboxedModule.require(MODULE_PATH, {
requires: {
bunyan: this.Bunyan,
'./log-level-checker': this.LogLevelChecker,
'./sentry-manager': sinon.stub().returns(this.SentryManager),
},
})
this.loggerName = 'test'
this.logger = this.LoggingManager.initialize(this.loggerName)
this.logger.initializeErrorReporting('test_dsn')
})
afterEach(function () {
@@ -154,6 +160,13 @@ describe('LoggingManager', function () {
})
})
describe('logger.error', function () {
it('should report errors to Sentry', function () {
this.logger.error({ foo: 'bar' }, 'message')
expect(this.SentryManager.captureExceptionRateLimited).to.have.been.called
})
})
describe('ringbuffer', function () {
beforeEach(function () {
this.logBufferMock = [
@@ -0,0 +1,247 @@
const Path = require('path')
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const sinon = require('sinon')
const MODULE_PATH = Path.join(__dirname, '../../sentry-manager.js')
describe('SentryManager', function () {
beforeEach(function () {
this.clock = sinon.useFakeTimers(Date.now())
this.Sentry = {
init: sinon.stub(),
captureException: sinon.stub(),
}
this.SentryManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'@sentry/node': this.Sentry,
},
})
this.sentryManager = new this.SentryManager('test_dsn')
})
afterEach(function () {
this.clock.restore()
})
describe('captureExceptionRateLimited', function () {
it('should report a single error to sentry', function () {
this.sentryManager.captureExceptionRateLimited({ foo: 'bar' }, 'message')
expect(this.Sentry.captureException).to.have.been.calledOnce
})
it('should report the same error to sentry only once', function () {
const error1 = new Error('this is the error')
this.sentryManager.captureExceptionRateLimited(
{ foo: error1 },
'first message'
)
this.sentryManager.captureExceptionRateLimited(
{ bar: error1 },
'second message'
)
expect(this.Sentry.captureException).to.have.been.calledOnce
})
it('should report two different errors to sentry individually', function () {
const error1 = new Error('this is the error')
const error2 = new Error('this is the error')
this.sentryManager.captureExceptionRateLimited(
{ foo: error1 },
'first message'
)
this.sentryManager.captureExceptionRateLimited(
{ bar: error2 },
'second message'
)
expect(this.Sentry.captureException).to.have.been.calledTwice
})
it('for multiple errors should only report a maximum of 5 errors to sentry', function () {
for (let i = 0; i < 10; i++) {
this.sentryManager.captureExceptionRateLimited(
{ foo: 'bar' },
'message'
)
}
expect(this.Sentry.captureException).to.have.callCount(5)
})
it('for multiple errors with a minute delay should report 10 errors to sentry', function () {
for (let i = 0; i < 10; i++) {
this.sentryManager.captureExceptionRateLimited(
{ foo: 'bar' },
'message'
)
}
expect(this.Sentry.captureException).to.have.callCount(5)
// allow a minute to pass
this.clock.tick(61 * 1000)
for (let i = 0; i < 10; i++) {
this.sentryManager.captureExceptionRateLimited(
{ foo: 'bar' },
'message'
)
}
expect(this.Sentry.captureException).to.have.callCount(10)
})
})
describe('captureException', function () {
it('should remove the path from fs errors', function () {
const fsError = new Error(
"Error: ENOENT: no such file or directory, stat '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'"
)
fsError.path = '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'
this.sentryManager.captureException({ err: fsError }, 'message', 'error')
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match.has(
'message',
'Error: ENOENT: no such file or directory, stat'
)
)
})
it('should sanitize error', function () {
const err = {
name: 'CustomError',
message: 'hello',
_oErrorTags: [{ stack: 'here:1', info: { one: 1 } }],
stack: 'here:0',
info: { key: 'value' },
code: 42,
signal: 9,
path: '/foo',
}
this.sentryManager.captureException({ err }, 'message', 'error')
const expectedErr = {
name: 'CustomError',
message: 'hello',
stack: 'here:0\nhere:1',
code: 42,
signal: 9,
path: '/foo',
}
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match(expectedErr),
sinon.match({
tags: sinon.match.any,
level: sinon.match.any,
extra: {
description: 'message',
info: sinon.match({
one: 1,
key: 'value',
}),
},
})
)
// Chai is very picky with comparing Error instances. Go the long way of comparing all the fields manually.
const gotErr = this.Sentry.captureException.args[0][0]
for (const [key, wanted] of Object.entries(expectedErr)) {
expect(gotErr).to.have.property(key, wanted)
}
})
it('should sanitize request', function () {
const req = {
ip: '1.2.3.4',
method: 'GET',
url: '/foo',
headers: {
referer: 'abc',
'content-length': 1337,
'user-agent': 'curl',
authorization: '42',
},
}
this.sentryManager.captureException({ req }, 'message', 'error')
const expectedReq = {
remoteAddress: '1.2.3.4',
method: 'GET',
url: '/foo',
headers: {
referer: 'abc',
'content-length': 1337,
'user-agent': 'curl',
},
}
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match({
message: 'message',
}),
sinon.match({
tags: sinon.match.any,
level: sinon.match.any,
extra: {
info: sinon.match.any,
req: expectedReq,
},
})
)
expect(this.Sentry.captureException.args[0][1].extra.req).to.deep.equal(
expectedReq
)
})
it('should sanitize response', function () {
const res = {
statusCode: 417,
body: Buffer.from('foo'),
getHeader(key) {
expect(key).to.be.oneOf(['content-length'])
if (key === 'content-length') return 1337
},
}
this.sentryManager.captureException({ res }, 'message', 'error')
const expectedRes = {
statusCode: 417,
headers: {
'content-length': 1337,
},
}
expect(this.Sentry.captureException).to.have.been.calledWith(
sinon.match({
message: 'message',
}),
sinon.match({
tags: sinon.match.any,
level: sinon.match.any,
extra: {
info: sinon.match.any,
res: expectedRes,
},
})
)
expect(this.Sentry.captureException.args[0][1].extra.res).to.deep.equal(
expectedRes
)
})
describe('reportedToSentry', function () {
it('should mark the error as reported to sentry', function () {
const err = new Error()
this.sentryManager.captureException({ err }, 'message')
expect(this.Sentry.captureException).to.have.been.called
expect(err.reportedToSentry).to.equal(true)
})
it('should mark two errors as reported to sentry', function () {
const err1 = new Error()
const err2 = new Error()
this.sentryManager.captureException({ err: err1, err2 }, 'message')
expect(this.Sentry.captureException).to.have.been.called
expect(err1.reportedToSentry).to.equal(true)
expect(err2.reportedToSentry).to.equal(true)
})
it('should not mark arbitrary objects as reported to sentry', function () {
const err = new Error()
const ctx = { foo: 'bar' }
this.sentryManager.captureException({ err, ctx }, 'message')
expect(this.Sentry.captureException).to.have.been.called
expect(ctx.reportedToSentry).not.to.exist
})
})
})
})
+1 -2
View File
@@ -2,7 +2,6 @@
"extends": "../../tsconfig.backend.json",
"include": [
"**/*.js",
"**/*.cjs",
"**/*.ts"
"**/*.cjs"
]
}
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
node_modules
.npmrc
-7
View File
@@ -1,7 +0,0 @@
# Autogenerated by build scripts. Do not edit.
.eslint*
.prettier*
libraries/metrics/**
package-lock.json
package.json
patches/**
-13
View File
@@ -1,13 +0,0 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
const all = {
require: 'test/setup.js',
...reporterOptions,
}
module.exports = all
+1 -1
View File
@@ -1 +1 @@
22.18.0
18.20.2
-82
View File
@@ -1,82 +0,0 @@
// Autogenerated by build scripts. Do not edit.
pipeline {
agent {
node {
label 'jenkins-agent-web'
customWorkspace '/workspace'
}
}
options {
timestamps()
timeout(time: 15, unit: 'MINUTES')
}
environment {
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
COMMIT_SHA = "${GIT_COMMIT}"
SHORT_SHA = "${GIT_COMMIT.take(7)}"
}
stages {
stage('Stage 1') {
parallel {
stage('Install monorepo') {
steps {
retry(count: 3) {
sh 'make monorepo_setup'
}
}
}
stage('Create reports folder') {
steps {
sh 'mkdir libraries/metrics/reports'
}
}
}
}
stage('Stage 2') {
parallel {
stage('Lint') {
steps {
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run lint -- --format json --output-file reports/eslint.json'
}
post {
always {
sh """
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/metrics/reports/eslint.json
"""
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'metrics-eslint', name: 'metrics eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/metrics/reports/eslint.json')]
}
}
}
stage('Format') {
steps {
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run format'
}
}
stage('Typecheck') {
steps {
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run types:check'
}
}
stage('Test') {
steps {
retry(count: 3) {
sh 'bin/run -w /overleaf/libraries/metrics monorepo npm run test:ci'
}
}
}
}
}
}
post {
always {
junit checksName: 'metrics test results', testResults: 'libraries/metrics/reports/junit-*.xml'
}
cleanup {
sh 'rm -rf libraries/metrics/reports'
sh 'make clean_jenkins -j10'
}
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
metrics
--dependencies=None
--docker-repos=gcr.io/overleaf-ops
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.18.0
--node-version=18.20.2
--public-repo=False
--script-version=4.5.0
+1 -1
View File
@@ -75,4 +75,4 @@ class RequestLogger {
}
}
module.exports = { monitor, RequestLogger }
module.exports.monitor = monitor
+1 -5
View File
@@ -5,8 +5,6 @@
* before any other module to support code instrumentation.
*/
const metricsModuleImportStartTime = performance.now()
const APP_NAME = process.env.METRICS_APP_NAME || 'unknown'
const BUILD_VERSION = process.env.BUILD_VERSION
const ENABLE_PROFILE_AGENT = process.env.ENABLE_PROFILE_AGENT === 'true'
@@ -90,7 +88,7 @@ function initializeProfileAgent() {
}
function initializePrometheus() {
const os = require('node:os')
const os = require('os')
const promClient = require('prom-client')
promClient.register.setDefaultLabels({ app: APP_NAME, host: os.hostname() })
promClient.collectDefaultMetrics({ timeout: 5000, prefix: '' })
@@ -105,5 +103,3 @@ function recordProcessStart() {
const metrics = require('.')
metrics.inc('process_startup')
}
module.exports = { metricsModuleImportStartTime }
+2 -2
View File
@@ -6,8 +6,8 @@
* logged along with the corresponding information from /proc/net/tcp.
*/
const fs = require('node:fs')
const diagnosticsChannel = require('node:diagnostics_channel')
const fs = require('fs')
const diagnosticsChannel = require('diagnostics_channel')
const SOCKET_MONITOR_INTERVAL = 60 * 1000
// set the threshold for logging leaked sockets in minutes, defaults to 15
+6 -6
View File
@@ -11,13 +11,13 @@ const seconds = 1000
// In Node 0.10 the default is 5, which means only 5 open connections at one.
// Node 0.12 has a default of Infinity. Make sure we have no limit set,
// regardless of Node version.
require('node:http').globalAgent.maxSockets = Infinity
require('node:https').globalAgent.maxSockets = Infinity
require('http').globalAgent.maxSockets = Infinity
require('https').globalAgent.maxSockets = Infinity
const SOCKETS_HTTP = require('node:http').globalAgent.sockets
const SOCKETS_HTTPS = require('node:https').globalAgent.sockets
const FREE_SOCKETS_HTTP = require('node:http').globalAgent.freeSockets
const FREE_SOCKETS_HTTPS = require('node:https').globalAgent.freeSockets
const SOCKETS_HTTP = require('http').globalAgent.sockets
const SOCKETS_HTTPS = require('https').globalAgent.sockets
const FREE_SOCKETS_HTTP = require('http').globalAgent.freeSockets
const FREE_SOCKETS_HTTPS = require('https').globalAgent.freeSockets
// keep track of set gauges and reset them in the next collection cycle
const SEEN_HOSTS_HTTP = new Set()
+5 -7
View File
@@ -9,7 +9,7 @@
"main": "index.js",
"dependencies": {
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0",
"@google-cloud/profiler": "^6.0.3",
"@google-cloud/profiler": "^6.0.0",
"@opentelemetry/api": "^1.4.1",
"@opentelemetry/auto-instrumentations-node": "^0.39.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.41.2",
@@ -23,18 +23,16 @@
"devDependencies": {
"bunyan": "^1.0.0",
"chai": "^4.3.6",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"mocha": "^10.2.0",
"sandboxed-module": "^2.0.4",
"sinon": "^9.2.4",
"typescript": "^5.0.4"
},
"scripts": {
"lint": "eslint --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"test:acceptance": "mocha --recursive --exit --grep=$MOCHA_GREP test/acceptance",
"test:acceptance": "mocha --reporter spec --recursive --exit --grep=$MOCHA_GREP test/acceptance",
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
@@ -1,6 +1,6 @@
const { promisify } = require('node:util')
const os = require('node:os')
const http = require('node:http')
const { promisify } = require('util')
const os = require('os')
const http = require('http')
const { expect } = require('chai')
const Metrics = require('../..')
@@ -316,7 +316,7 @@ async function checkSummaryValues(key, values) {
for (const quantile of Object.keys(values)) {
expect(found[quantile]).to.be.within(
values[quantile] - 5,
values[quantile] + 15,
values[quantile] + 5,
`quantile: ${quantile}`
)
}
@@ -1,8 +0,0 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
includePending: true,
jenkinsMode: true,
},
}
-1
View File
@@ -1 +0,0 @@
// no setup yet
+1 -1
View File
@@ -5,7 +5,7 @@
*/
const chai = require('chai')
const { expect } = chai
const path = require('node:path')
const path = require('path')
const modulePath = path.join(__dirname, '../../../event_loop.js')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
+1 -1
View File
@@ -1,4 +1,4 @@
const Path = require('node:path')
const Path = require('path')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
+1 -2
View File
@@ -2,7 +2,6 @@
"extends": "../../tsconfig.backend.json",
"include": [
"**/*.js",
"**/*.cjs",
"**/*.ts"
"**/*.cjs"
]
}
@@ -1,7 +0,0 @@
# Autogenerated by build scripts. Do not edit.
.eslint*
.prettier*
libraries/mongo-utils/**
package-lock.json
package.json
patches/**
-1
View File
@@ -1 +0,0 @@
22.18.0
-72
View File
@@ -1,72 +0,0 @@
// Autogenerated by build scripts. Do not edit.
pipeline {
agent {
node {
label 'jenkins-agent-web'
customWorkspace '/workspace'
}
}
options {
timestamps()
timeout(time: 15, unit: 'MINUTES')
}
environment {
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
COMMIT_SHA = "${GIT_COMMIT}"
SHORT_SHA = "${GIT_COMMIT.take(7)}"
}
stages {
stage('Stage 1') {
parallel {
stage('Install monorepo') {
steps {
retry(count: 3) {
sh 'make monorepo_setup'
}
}
}
stage('Create reports folder') {
steps {
sh 'mkdir libraries/mongo-utils/reports'
}
}
}
}
stage('Stage 2') {
parallel {
stage('Lint') {
steps {
sh 'bin/run -w /overleaf/libraries/mongo-utils monorepo npm run lint -- --format json --output-file reports/eslint.json'
}
post {
always {
sh """
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/mongo-utils/reports/eslint.json
"""
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'mongo-utils-eslint', name: 'mongo-utils eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/mongo-utils/reports/eslint.json')]
}
}
}
stage('Format') {
steps {
sh 'bin/run -w /overleaf/libraries/mongo-utils monorepo npm run format'
}
}
stage('Typecheck') {
steps {
sh 'bin/run -w /overleaf/libraries/mongo-utils monorepo npm run types:check'
}
}
}
}
}
post {
cleanup {
sh 'rm -rf libraries/mongo-utils/reports'
sh 'make clean_jenkins -j10'
}
}
}
-321
View File
@@ -1,321 +0,0 @@
// @ts-check
/* eslint-disable no-console */
const { ObjectId, ReadPreference } = require('mongodb')
const READ_PREFERENCE_SECONDARY =
process.env.MONGO_HAS_SECONDARIES === 'true'
? ReadPreference.secondary.mode
: ReadPreference.secondaryPreferred.mode
const ONE_MONTH_IN_MS = 1000 * 60 * 60 * 24 * 31
let ID_EDGE_PAST
const ID_EDGE_FUTURE = objectIdFromMs(Date.now() + 1000)
let BATCH_DESCENDING
let BATCH_SIZE
let VERBOSE_LOGGING
let BATCH_RANGE_START
let BATCH_RANGE_END
let BATCH_MAX_TIME_SPAN_IN_MS
let BATCHED_UPDATE_RUNNING = false
/**
* @typedef {import("mongodb").Collection} Collection
* @typedef {import("mongodb-legacy").Collection} LegacyCollection
* @typedef {import("mongodb").Document} Document
* @typedef {import("mongodb").FindOptions} FindOptions
* @typedef {import("mongodb").UpdateFilter<Document>} UpdateDocument
*/
/**
* @typedef {Object} BatchedUpdateOptions
* @property {string} [BATCH_DESCENDING]
* @property {string} [BATCH_LAST_ID]
* @property {string} [BATCH_MAX_TIME_SPAN_IN_MS]
* @property {string} [BATCH_RANGE_END]
* @property {string} [BATCH_RANGE_START]
* @property {string} [BATCH_SIZE]
* @property {string} [VERBOSE_LOGGING]
* @property {(progress: string) => Promise<void>} [trackProgress]
*/
/**
* @param {BatchedUpdateOptions} options
*/
function refreshGlobalOptionsForBatchedUpdate(options = {}) {
options = Object.assign({}, options, process.env)
BATCH_DESCENDING = options.BATCH_DESCENDING === 'true'
BATCH_SIZE = parseInt(options.BATCH_SIZE || '1000', 10) || 1000
VERBOSE_LOGGING = options.VERBOSE_LOGGING === 'true'
if (options.BATCH_LAST_ID) {
BATCH_RANGE_START = objectIdFromInput(options.BATCH_LAST_ID)
} else if (options.BATCH_RANGE_START) {
BATCH_RANGE_START = objectIdFromInput(options.BATCH_RANGE_START)
} else {
if (BATCH_DESCENDING) {
BATCH_RANGE_START = ID_EDGE_FUTURE
} else {
BATCH_RANGE_START = ID_EDGE_PAST
}
}
BATCH_MAX_TIME_SPAN_IN_MS = parseInt(
options.BATCH_MAX_TIME_SPAN_IN_MS || ONE_MONTH_IN_MS.toString(),
10
)
if (options.BATCH_RANGE_END) {
BATCH_RANGE_END = objectIdFromInput(options.BATCH_RANGE_END)
} else {
if (BATCH_DESCENDING) {
BATCH_RANGE_END = ID_EDGE_PAST
} else {
BATCH_RANGE_END = ID_EDGE_FUTURE
}
}
}
/**
* @param {Collection | LegacyCollection} collection
* @param {Document} query
* @param {ObjectId} start
* @param {ObjectId} end
* @param {Document} projection
* @param {FindOptions} findOptions
* @return {Promise<Array<Document>>}
*/
async function getNextBatch(
collection,
query,
start,
end,
projection,
findOptions
) {
if (BATCH_DESCENDING) {
query._id = {
$gt: end,
$lte: start,
}
} else {
query._id = {
$gt: start,
$lte: end,
}
}
return await collection
.find(query, findOptions)
.project(projection)
.sort({ _id: BATCH_DESCENDING ? -1 : 1 })
.limit(BATCH_SIZE)
.toArray()
}
/**
* @param {Collection | LegacyCollection} collection
* @param {Array<Document>} nextBatch
* @param {UpdateDocument} update
* @return {Promise<void>}
*/
async function performUpdate(collection, nextBatch, update) {
await collection.updateMany(
{ _id: { $in: nextBatch.map(entry => entry._id) } },
update
)
}
/**
* @param {string} input
* @return {ObjectId}
*/
function objectIdFromInput(input) {
if (input.includes('T')) {
const t = new Date(input).getTime()
if (Number.isNaN(t)) throw new Error(`${input} is not a valid date`)
return objectIdFromMs(t)
} else {
return new ObjectId(input)
}
}
/**
* @param {ObjectId} objectId
* @return {string}
*/
function renderObjectId(objectId) {
return `${objectId} (${objectId.getTimestamp().toISOString()})`
}
/**
* @param {number} ms
* @return {ObjectId}
*/
function objectIdFromMs(ms) {
return ObjectId.createFromTime(ms / 1000)
}
/**
* @param {ObjectId} id
* @return {number}
*/
function getMsFromObjectId(id) {
return id.getTimestamp().getTime()
}
/**
* @param {ObjectId} start
* @return {ObjectId}
*/
function getNextEnd(start) {
let end
if (BATCH_DESCENDING) {
end = objectIdFromMs(getMsFromObjectId(start) - BATCH_MAX_TIME_SPAN_IN_MS)
if (getMsFromObjectId(end) <= getMsFromObjectId(BATCH_RANGE_END)) {
end = BATCH_RANGE_END
}
} else {
end = objectIdFromMs(getMsFromObjectId(start) + BATCH_MAX_TIME_SPAN_IN_MS)
if (getMsFromObjectId(end) >= getMsFromObjectId(BATCH_RANGE_END)) {
end = BATCH_RANGE_END
}
}
return end
}
/**
* @param {Collection | LegacyCollection} collection
* @return {Promise<ObjectId|null>}
*/
async function getIdEdgePast(collection) {
const [first] = await collection
.find({})
.project({ _id: 1 })
.sort({ _id: 1 })
.limit(1)
.toArray()
if (!first) return null
// Go one second further into the past in order to include the first entry via
// first._id > ID_EDGE_PAST
return objectIdFromMs(Math.max(0, getMsFromObjectId(first._id) - 1000))
}
/**
* @param {Collection | LegacyCollection} collection
* @param {Document} query
* @param {UpdateDocument | ((batch: Array<Document>) => Promise<void>)} update
* @param {Document} [projection]
* @param {FindOptions} [findOptions]
* @param {BatchedUpdateOptions} [batchedUpdateOptions]
*/
async function batchedUpdate(
collection,
query,
update,
projection,
findOptions,
batchedUpdateOptions = {}
) {
// only a single batchedUpdate can run at a time due to global variables
if (BATCHED_UPDATE_RUNNING) {
throw new Error('batchedUpdate is already running')
}
try {
BATCHED_UPDATE_RUNNING = true
ID_EDGE_PAST = await getIdEdgePast(collection)
if (!ID_EDGE_PAST) {
console.warn(
`The collection ${collection.collectionName} appears to be empty.`
)
return 0
}
refreshGlobalOptionsForBatchedUpdate(batchedUpdateOptions)
const { trackProgress = async progress => console.warn(progress) } =
batchedUpdateOptions
findOptions = findOptions || {}
findOptions.readPreference = READ_PREFERENCE_SECONDARY
projection = projection || { _id: 1 }
let nextBatch
let updated = 0
let start = BATCH_RANGE_START
while (start !== BATCH_RANGE_END) {
let end = getNextEnd(start)
nextBatch = await getNextBatch(
collection,
query,
start,
end,
projection,
findOptions
)
if (nextBatch.length > 0) {
end = nextBatch[nextBatch.length - 1]._id
updated += nextBatch.length
if (VERBOSE_LOGGING) {
console.log(
`Running update on batch with ids ${JSON.stringify(
nextBatch.map(entry => entry._id)
)}`
)
}
await trackProgress(
`Running update on batch ending ${renderObjectId(end)}`
)
if (typeof update === 'function') {
await update(nextBatch)
} else {
await performUpdate(collection, nextBatch, update)
}
}
await trackProgress(`Completed batch ending ${renderObjectId(end)}`)
start = end
}
return updated
} finally {
BATCHED_UPDATE_RUNNING = false
}
}
/**
* @param {Collection | LegacyCollection} collection
* @param {Document} query
* @param {UpdateDocument | ((batch: Array<Object>) => Promise<void>)} update
* @param {Document} [projection]
* @param {FindOptions} [findOptions]
* @param {BatchedUpdateOptions} [batchedUpdateOptions]
*/
function batchedUpdateWithResultHandling(
collection,
query,
update,
projection,
findOptions,
batchedUpdateOptions
) {
batchedUpdate(
collection,
query,
update,
projection,
findOptions,
batchedUpdateOptions
)
.then(processed => {
console.error({ processed })
process.exit(0)
})
.catch(error => {
console.error({ error })
process.exit(1)
})
}
module.exports = {
READ_PREFERENCE_SECONDARY,
objectIdFromInput,
renderObjectId,
batchedUpdate,
batchedUpdateWithResultHandling,
}
-8
View File
@@ -1,8 +0,0 @@
mongo-utils
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.18.0
--public-repo=False
-5
View File
@@ -1,5 +0,0 @@
// @ts-check
const { cleanupTestDatabase, dropTestDatabase } = require('./test-utils')
module.exports = { cleanupTestDatabase, dropTestDatabase }
-32
View File
@@ -1,32 +0,0 @@
{
"name": "@overleaf/mongo-utils",
"version": "0.0.1",
"description": "utilities to help working with mongo",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"lint": "eslint --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
"test:ci": "npm run test:unit",
"types:check": "tsc --noEmit"
},
"author": "Overleaf (https://www.overleaf.com)",
"license": "AGPL-3.0-only",
"dependencies": {
"mongodb": "6.12.0",
"mongodb-legacy": "6.1.3"
},
"devDependencies": {
"chai": "^4.3.6",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"sandboxed-module": "^2.0.4",
"sinon": "^9.2.4",
"sinon-chai": "^3.7.0",
"typescript": "^5.0.4"
}
}
-55
View File
@@ -1,55 +0,0 @@
// @ts-check
/**
* @import { MongoClient } from 'mongodb'
* @import { MongoClient as LegacyMongoClient } from 'mongodb-legacy'
*/
/**
* Delete all data from the test Mongo database
*
* This doesn't drop the collections, so indexes are preserved.
*
* @param {MongoClient | LegacyMongoClient} mongoClient
*/
async function cleanupTestDatabase(mongoClient) {
ensureTestDatabase(mongoClient)
const db = mongoClient.db()
const allCollections = await db.collections()
const collections = allCollections.filter(
coll => coll.collectionName !== 'migrations'
)
await Promise.all(collections.map(coll => coll.deleteMany({})))
}
/**
* Drop the test Monto database
*
* This drops the whole database, including indexes.
*
* @param {MongoClient | LegacyMongoClient } mongoClient
*/
async function dropTestDatabase(mongoClient) {
ensureTestDatabase(mongoClient)
await mongoClient.db().dropDatabase()
}
/**
* Ensure that the given client is connected to a test database.
*
* This should be called before performing destructive operations on the test
* database.
*
* @param {MongoClient | LegacyMongoClient } mongoClient
*/
function ensureTestDatabase(mongoClient) {
const dbName = mongoClient.db().databaseName
const env = process.env.NODE_ENV
if (dbName !== 'test-overleaf' || env !== 'test') {
throw new Error(
`Refusing to clear database '${dbName}' in environment '${env}'`
)
}
}
module.exports = { cleanupTestDatabase, dropTestDatabase }
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.backend.json",
"include": [
"**/*.js",
"**/*.cjs",
"**/*.ts"
]
}
+1
View File
@@ -0,0 +1 @@
node_modules/
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+5
View File
@@ -0,0 +1,5 @@
.nyc_output
coverage
node_modules/
.npmrc
-7
View File
@@ -1,7 +0,0 @@
# Autogenerated by build scripts. Do not edit.
.eslint*
.prettier*
libraries/o-error/**
package-lock.json
package.json
patches/**
-13
View File
@@ -1,13 +0,0 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
const all = {
require: 'test/setup.js',
...reporterOptions,
}
module.exports = all
+1 -1
View File
@@ -1 +1 @@
22.18.0
18.20.2
-82
View File
@@ -1,82 +0,0 @@
// Autogenerated by build scripts. Do not edit.
pipeline {
agent {
node {
label 'jenkins-agent-web'
customWorkspace '/workspace'
}
}
options {
timestamps()
timeout(time: 15, unit: 'MINUTES')
}
environment {
BRANCH_NAME = "${env.CHANGE_BRANCH ? env.CHANGE_BRANCH : env.BRANCH_NAME}"
JENKINS_BUILD_NUMBER = "${BUILD_NUMBER}"
BUILD_NUMBER = "${SHORT_SHA}_${BUILD_NUMBER}"
COMMIT_SHA = "${GIT_COMMIT}"
SHORT_SHA = "${GIT_COMMIT.take(7)}"
}
stages {
stage('Stage 1') {
parallel {
stage('Install monorepo') {
steps {
retry(count: 3) {
sh 'make monorepo_setup'
}
}
}
stage('Create reports folder') {
steps {
sh 'mkdir libraries/o-error/reports'
}
}
}
}
stage('Stage 2') {
parallel {
stage('Lint') {
steps {
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run lint -- --format json --output-file reports/eslint.json'
}
post {
always {
sh """
sed -i 's_"filePath":"/overleaf_"filePath":"/workspace_g' libraries/o-error/reports/eslint.json
"""
recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'o-error-eslint', name: 'o-error eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'libraries/o-error/reports/eslint.json')]
}
}
}
stage('Format') {
steps {
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run format'
}
}
stage('Typecheck') {
steps {
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run types:check'
}
}
stage('Test') {
steps {
retry(count: 3) {
sh 'bin/run -w /overleaf/libraries/o-error monorepo npm run test:ci'
}
}
}
}
}
}
post {
always {
junit checksName: 'o-error test results', testResults: 'libraries/o-error/reports/junit-*.xml'
}
cleanup {
sh 'rm -rf libraries/o-error/reports'
sh 'make clean_jenkins -j10'
}
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
o-error
--dependencies=None
--docker-repos=gcr.io/overleaf-ops
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.18.0
--node-version=18.20.2
--public-repo=False
--script-version=4.5.0
+1 -1
View File
@@ -70,7 +70,7 @@ sayHi3(43, (err, result) => {
}
})
const promisify = require('node:util').promisify
const promisify = require('util').promisify
demoDatabase.findUserAsync = promisify(demoDatabase.findUser)
async function sayHi4NoHandling(userId) {
+10 -27
View File
@@ -1,34 +1,20 @@
// @ts-check
/**
* Light-weight helpers for handling JavaScript Errors in node.js and the
* browser.
*/
class OError extends Error {
/**
* The error that is the underlying cause of this error
*
* @type {unknown}
*/
cause
/**
* List of errors encountered as the callback chain is unwound
*
* @type {TaggedError[] | undefined}
*/
_oErrorTags
/**
* @param {string} message as for built-in Error
* @param {Object} [info] extra data to attach to the error
* @param {unknown} [cause] the internal error that caused this error
* @param {Error} [cause] the internal error that caused this error
*/
constructor(message, info, cause) {
super(message)
this.name = this.constructor.name
if (info) this.info = info
if (cause) this.cause = cause
/** @private @type {Array<TaggedError> | undefined} */
this._oErrorTags // eslint-disable-line
}
/**
@@ -45,7 +31,7 @@ class OError extends Error {
/**
* Wrap the given error, which caused this error.
*
* @param {unknown} cause the internal error that caused this error
* @param {Error} cause the internal error that caused this error
* @return {this}
*/
withCause(cause) {
@@ -79,16 +65,13 @@ class OError extends Error {
* }
* }
*
* @template {unknown} E
* @param {E} error the error to tag
* @param {Error} error the error to tag
* @param {string} [message] message with which to tag `error`
* @param {Object} [info] extra data with wich to tag `error`
* @return {E} the modified `error` argument
* @return {Error} the modified `error` argument
*/
static tag(error, message, info) {
const oError = /** @type {{ _oErrorTags: TaggedError[] | undefined }} */ (
error
)
const oError = /** @type{OError} */ (error)
if (!oError._oErrorTags) oError._oErrorTags = []
@@ -119,7 +102,7 @@ class OError extends Error {
*
* If an info property is repeated, the last one wins.
*
* @param {unknown} error any error (may or may not be an `OError`)
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
* @return {Object}
*/
static getFullInfo(error) {
@@ -146,7 +129,7 @@ class OError extends Error {
* Return the `stack` property from `error`, including the `stack`s for any
* tagged errors added with `OError.tag` and for any `cause`s.
*
* @param {unknown} error any error (may or may not be an `OError`)
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
* @return {string}
*/
static getFullStack(error) {
@@ -160,7 +143,7 @@ class OError extends Error {
stack += `\n${oError._oErrorTags.map(tag => tag.stack).join('\n')}`
}
const causeStack = OError.getFullStack(oError.cause)
const causeStack = oError.cause && OError.getFullStack(oError.cause)
if (causeStack) {
stack += '\ncaused by:\n' + indent(causeStack)
}
+4 -6
View File
@@ -18,8 +18,8 @@
],
"scripts": {
"build": "npm run --silent test",
"lint": "eslint --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
@@ -32,11 +32,9 @@
"repository": "github:overleaf/o-error",
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/node": "^18.19.123",
"@types/node": "^18.17.4",
"chai": "^4.3.6",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"mocha": "^10.2.0",
"typescript": "^5.0.4"
}
}
@@ -1,8 +0,0 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
includePending: true,
jenkinsMode: true,
},
}
@@ -1,7 +1,7 @@
const { expect } = require('chai')
const { promisify } = require('node:util')
const { promisify } = require('util')
const OError = require('../..')
const OError = require('..')
const {
expectError,
@@ -268,11 +268,6 @@ describe('utils', function () {
expect(OError.getFullInfo(null)).to.deep.equal({})
})
it('works when given a string', function () {
const err = 'not an error instance'
expect(OError.getFullInfo(err)).to.deep.equal({})
})
it('works on a normal error', function () {
const err = new Error('foo')
expect(OError.getFullInfo(err)).to.deep.equal({})

Some files were not shown because too many files have changed in this diff Show More