diff --git a/.eslintrc.js b/.eslintrc.js index fb85a9b286..684333e675 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,7 +34,11 @@ module.exports = { parserOptions: { ecmaVersion: 2022 }, - ignorePatterns: ['test/versioned-external'], + ignorePatterns: [ + 'test/versioned-external', + 'test/versioned/nextjs/app', + 'test/versioned/nextjs/app-dir' + ], overrides: [ { files: ['**/*.mjs'], diff --git a/.github/workflows/benchmark-tests.yml b/.github/workflows/benchmark-tests.yml index 52f4d07970..8d2b43f439 100644 --- a/.github/workflows/benchmark-tests.yml +++ b/.github/workflows/benchmark-tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 6de23499a3..1ba1b55d43 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -98,7 +98,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 @@ -130,7 +130,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 @@ -166,7 +166,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 @@ -217,7 +217,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 @@ -240,7 +240,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/smoke-test-workflow.yml b/.github/workflows/smoke-test-workflow.yml index 58a367830a..2d02dc76e6 100644 --- a/.github/workflows/smoke-test-workflow.yml +++ b/.github/workflows/smoke-test-workflow.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/versioned-coverage.yml b/.github/workflows/versioned-coverage.yml deleted file mode 100644 index e7899aeeab..0000000000 --- a/.github/workflows/versioned-coverage.yml +++ /dev/null @@ -1,38 +0,0 @@ -# This workflow is intended to be used to run versioned tests for different scenarios(i.e.- legacy context manager, etc) - -name: Nightly Versioned Scenario Runs - -on: - workflow_dispatch: - schedule: - - cron: '0 9 * * 1-5' - -env: - # Enable versioned runner quiet mode to make CI output easier to read: - OUTPUT_MODE: quiet - -jobs: - legacy-context: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - node-version: [16.x, 18.x, 20.x, 22.x] - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - name: Install Dependencies - run: npm install - - name: Run Docker Services - run: npm run services - - name: Run Legacy Context Versioned Tests - run: TEST_CHILD_TIMEOUT=600000 npm run versioned:legacy-context - env: - VERSIONED_MODE: --major - JOBS: 4 # 2 per CPU seems to be the sweet spot in GHA (July 2022) - SKIP_C8: true diff --git a/.github/workflows/versioned-security-agent.yml b/.github/workflows/versioned-security-agent.yml index 918901c877..535cf28274 100644 --- a/.github/workflows/versioned-security-agent.yml +++ b/.github/workflows/versioned-security-agent.yml @@ -63,7 +63,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 66539ab912..7b41ec1e65 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,37 @@ If you cannot control how your program is run, you can load the `newrelic` modul /* ... the rest of your program ... */ ``` +## Next.js instrumentation +**Note**: The minimum supported Next.js version is [12.0.9](https://github.com/vercel/next.js/releases/tag/v12.0.9). If you are using Next.js middleware the minimum supported version is [12.2.0](https://github.com/vercel/next.js/releases/tag/v12.2.0). + +The New Relic Node.js agent provides instrumentation for Next.js The instrumentation provides telemetry for server-side rendering via [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props), [middleware](https://nextjs.org/docs/middleware), and New Relic transaction naming for both page and server requests. It does not provide any instrumentation for actions occurring during build or in client-side code. If you want telemetry data on actions occurring on the client (browser), you can [inject the browser agent](./documentation/nextjs/faqs/browser-agent.md). + +Here are documents for more in-depth explanations about [transaction naming](./documentation/nextjs/transactions.md), and [segments/spans](./documentation/nextjs/segments-and-spans.md). + + +### Setup +Typically you are running a Next.js app with the `next` cli and you must load the agent via `NODE_OPTIONS`: + +```sh +NODE_OPTIONS='-r newrelic' next start +``` + +If you are having trouble getting the `newrelic` package to instrument Next.js, take a look at our [FAQs](./documentation/nextjs/faqs/README.md). + +### Next.js example projects +The following example applications show how to load the `newrelic` instrumentation, inject browser agent, and handle errors: + + * [Pages Router example](https://github.com/newrelic/newrelic-node-examples/tree/58f760e828c45d90391bda3f66764d4420ba4990/nextjs-legacy) + * [App Router example](https://github.com/newrelic/newrelic-node-examples/tree/58f760e828c45d90391bda3f66764d4420ba4990/nextjs-app-router) + +### Custom Next.js servers + +If you are using next as a [custom server](https://nextjs.org/docs/advanced-features/custom-server), you're probably not running your application with the `next` CLI. In that scenario we recommend running the Next.js instrumentation as follows. + +```sh +node -r newrelic your-program.js +``` + ## ECMAScript Modules If your application is written with `import` and `export` statements in javascript, you are using [ES Modules](https://nodejs.org/api/esm.html#modules-ecmascript-modules) and must bootstrap the agent in a different way. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 5adeba5057..146a279d6d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -44,6 +44,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic * [ajv](#ajv) * [async](#async) * [aws-sdk](#aws-sdk) +* [borp](#borp) * [c8](#c8) * [clean-jsdoc-theme](#clean-jsdoc-theme) * [commander](#commander) @@ -91,7 +92,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic ### @grpc/grpc-js -This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.10.8](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.10.8)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.10.8/LICENSE): +This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.11.1](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.11.1)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.11.1/LICENSE): ``` Apache License @@ -539,7 +540,7 @@ SOFTWARE. ### @newrelic/security-agent -This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v1.3.0](https://github.com/newrelic/csec-node-agent/tree/v1.3.0)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v1.3.0/LICENSE): +This product includes source derived from [@newrelic/security-agent](https://github.com/newrelic/csec-node-agent) ([v1.4.0](https://github.com/newrelic/csec-node-agent/tree/v1.4.0)), distributed under the [UNKNOWN License](https://github.com/newrelic/csec-node-agent/blob/v1.4.0/LICENSE): ``` ## New Relic Software License v1.0 @@ -645,7 +646,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### https-proxy-agent -This product includes source derived from [https-proxy-agent](https://github.com/TooTallNate/proxy-agents) ([v7.0.4](https://github.com/TooTallNate/proxy-agents/tree/v7.0.4)), distributed under the [MIT License](https://github.com/TooTallNate/proxy-agents/blob/v7.0.4/LICENSE): +This product includes source derived from [https-proxy-agent](https://github.com/TooTallNate/proxy-agents) ([v7.0.5](https://github.com/TooTallNate/proxy-agents/tree/v7.0.5)), distributed under the [MIT License](https://github.com/TooTallNate/proxy-agents/blob/v7.0.5/LICENSE): ``` (The MIT License) @@ -674,22 +675,210 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### import-in-the-middle -This product includes source derived from [import-in-the-middle](https://github.com/DataDog/import-in-the-middle) ([v1.8.0](https://github.com/DataDog/import-in-the-middle/tree/v1.8.0)), distributed under the [Apache-2.0 License](https://github.com/DataDog/import-in-the-middle/blob/v1.8.0/LICENSE): +This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.9.1](https://github.com/nodejs/import-in-the-middle/tree/v1.9.1)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.9.1/LICENSE): ``` -Copyright 2021 Datadog, Inc. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. - http://www.apache.org/licenses/LICENSE-2.0 + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. ``` @@ -830,7 +1019,7 @@ IN THE SOFTWARE. ### semver -This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.6.2](https://github.com/npm/node-semver/tree/v7.6.2)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.6.2/LICENSE): +This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.6.3](https://github.com/npm/node-semver/tree/v7.6.3)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.6.3/LICENSE): ``` The ISC License @@ -853,7 +1042,7 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ### winston-transport -This product includes source derived from [winston-transport](https://github.com/winstonjs/winston-transport) ([v4.7.0](https://github.com/winstonjs/winston-transport/tree/v4.7.0)), distributed under the [MIT License](https://github.com/winstonjs/winston-transport/blob/v4.7.0/LICENSE): +This product includes source derived from [winston-transport](https://github.com/winstonjs/winston-transport) ([v4.7.1](https://github.com/winstonjs/winston-transport/tree/v4.7.1)), distributed under the [MIT License](https://github.com/winstonjs/winston-transport/blob/v4.7.1/LICENSE): ``` The MIT License (MIT) @@ -886,7 +1075,7 @@ SOFTWARE. ### @aws-sdk/client-s3 -This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.592.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.592.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.592.0/LICENSE): +This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.616.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.616.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.616.0/LICENSE): ``` Apache License @@ -1095,7 +1284,7 @@ This product includes source derived from [@aws-sdk/client-s3](https://github.co ### @aws-sdk/s3-request-presigner -This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.592.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.592.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.592.0/LICENSE): +This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.616.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.616.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.616.0/LICENSE): ``` Apache License @@ -1750,7 +1939,7 @@ This product includes source derived from [@newrelic/newrelic-oss-cli](https://g ### @newrelic/test-utilities -This product includes source derived from [@newrelic/test-utilities](https://github.com/newrelic/node-test-utilities) ([v8.6.0](https://github.com/newrelic/node-test-utilities/tree/v8.6.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-test-utilities/blob/v8.6.0/LICENSE): +This product includes source derived from [@newrelic/test-utilities](https://github.com/newrelic/node-test-utilities) ([v8.7.0](https://github.com/newrelic/node-test-utilities/tree/v8.7.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-test-utilities/blob/v8.7.0/LICENSE): ``` Apache License @@ -1989,7 +2178,7 @@ THE SOFTWARE. ### @slack/bolt -This product includes source derived from [@slack/bolt](https://github.com/slackapi/bolt) ([v3.18.0](https://github.com/slackapi/bolt/tree/v3.18.0)), distributed under the [MIT License](https://github.com/slackapi/bolt/blob/v3.18.0/LICENSE): +This product includes source derived from [@slack/bolt](https://github.com/slackapi/bolt) ([v3.19.0](https://github.com/slackapi/bolt/tree/v3.19.0)), distributed under the [MIT License](https://github.com/slackapi/bolt/blob/v3.19.0/LICENSE): ``` The MIT License (MIT) @@ -2493,7 +2682,7 @@ THE SOFTWARE. ### aws-sdk -This product includes source derived from [aws-sdk](https://github.com/aws/aws-sdk-js) ([v2.1636.0](https://github.com/aws/aws-sdk-js/tree/v2.1636.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js/blob/v2.1636.0/LICENSE.txt): +This product includes source derived from [aws-sdk](https://github.com/aws/aws-sdk-js) ([v2.1659.0](https://github.com/aws/aws-sdk-js/tree/v2.1659.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js/blob/v2.1659.0/LICENSE.txt): ``` @@ -2701,6 +2890,35 @@ This product includes source derived from [aws-sdk](https://github.com/aws/aws-s ``` +### borp + +This product includes source derived from [borp](https://github.com/mcollina/borp) ([v0.17.0](https://github.com/mcollina/borp/tree/v0.17.0)), distributed under the [MIT License](https://github.com/mcollina/borp/blob/v0.17.0/LICENSE): + +``` +MIT License + +Copyright (c) 2023 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ### c8 This product includes source derived from [c8](https://github.com/bcoe/c8) ([v8.0.1](https://github.com/bcoe/c8/tree/v8.0.1)), distributed under the [ISC License](https://github.com/bcoe/c8/blob/v8.0.1/LICENSE.txt): @@ -2892,7 +3110,7 @@ THE SOFTWARE. ### eslint-plugin-jsdoc -This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v48.2.8](https://github.com/gajus/eslint-plugin-jsdoc/tree/v48.2.8)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v48.2.8/LICENSE): +This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v48.8.0](https://github.com/gajus/eslint-plugin-jsdoc/tree/v48.8.0)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v48.8.0/LICENSE): ``` Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/) @@ -3474,7 +3692,7 @@ SOFTWARE. ### lockfile-lint -This product includes source derived from [lockfile-lint](https://github.com/lirantal/lockfile-lint) ([v4.13.2](https://github.com/lirantal/lockfile-lint/tree/v4.13.2)), distributed under the [Apache-2.0 License](https://github.com/lirantal/lockfile-lint/blob/v4.13.2/LICENSE): +This product includes source derived from [lockfile-lint](https://github.com/lirantal/lockfile-lint) ([v4.14.0](https://github.com/lirantal/lockfile-lint/tree/v4.14.0)), distributed under the [Apache-2.0 License](https://github.com/lirantal/lockfile-lint/blob/v4.14.0/LICENSE): ``` @@ -3701,18 +3919,31 @@ SOFTWARE. ### proxy -This product includes source derived from [proxy](https://github.com/TooTallNate/proxy-agents) ([v2.1.1](https://github.com/TooTallNate/proxy-agents/tree/v2.1.1)), distributed under the [MIT License](https://github.com/TooTallNate/proxy-agents/blob/v2.1.1/README.md): +This product includes source derived from [proxy](https://github.com/TooTallNate/proxy-agents) ([v2.2.0](https://github.com/TooTallNate/proxy-agents/tree/v2.2.0)), distributed under the [MIT License](https://github.com/TooTallNate/proxy-agents/blob/v2.2.0/LICENSE): ``` -MIT License +(The MIT License) -Copyright (c) +Copyright (c) 2013 Nathan Rajlich -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ### proxyquire @@ -3748,7 +3979,7 @@ OTHER DEALINGS IN THE SOFTWARE. ### rfdc -This product includes source derived from [rfdc](https://github.com/davidmarkclements/rfdc) ([v1.3.1](https://github.com/davidmarkclements/rfdc/tree/v1.3.1)), distributed under the [MIT License](https://github.com/davidmarkclements/rfdc/blob/v1.3.1/LICENSE): +This product includes source derived from [rfdc](https://github.com/davidmarkclements/rfdc) ([v1.4.1](https://github.com/davidmarkclements/rfdc/tree/v1.4.1)), distributed under the [MIT License](https://github.com/davidmarkclements/rfdc/blob/v1.4.1/LICENSE): ``` Copyright 2019 "David Mark Clements " @@ -3967,7 +4198,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### @newrelic/native-metrics -This product includes source derived from [@newrelic/native-metrics](https://github.com/newrelic/node-native-metrics) ([v10.1.1](https://github.com/newrelic/node-native-metrics/tree/v10.1.1)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-native-metrics/blob/v10.1.1/LICENSE): +This product includes source derived from [@newrelic/native-metrics](https://github.com/newrelic/node-native-metrics) ([v10.2.0](https://github.com/newrelic/node-native-metrics/tree/v10.2.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-native-metrics/blob/v10.2.0/LICENSE): ``` Apache License diff --git a/docker-compose.yml b/docker-compose.yml index 18036638ea..849bb27f5f 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,23 +60,11 @@ services: ports: - "11211:11211" - mongodb_3: - container_name: nr_node_mongodb - platform: ${DOCKER_PLATFORM:-linux/amd64} - image: library/mongo:3 - ports: - - "27017:27017" - healthcheck: - test: ["CMD", "mongo", "--quiet"] - interval: 1s - timeout: 10s - retries: 30 - mongodb_5: container_name: nr_node_mongodb_5 image: library/mongo:5 ports: - - "27018:27017" + - "27017:27017" healthcheck: test: ["CMD", "mongo", "--quiet"] interval: 1s diff --git a/documentation/feature-flags.md b/documentation/feature-flags.md index 22b8a8d438..1a45b72f3d 100644 --- a/documentation/feature-flags.md +++ b/documentation/feature-flags.md @@ -33,12 +33,6 @@ Any prerelease flags can be enabled or disabled in your agent config by adding a * Description: Now that `new_promise_tracking` is the default async context tracking behavior in the agent, `unresolved_promise_cleanup` is enabled by default. Disabling it can help with performance of agent when an application creates many promises. * **WARNING**: If you set `unresolved_promise_cleanup` to `false`, failure to resolve all promises in your application will result in memory leaks even if those promises are garbage collected. -#### legacy_context_manager -* Enabled by default: `false` -* Configuration: `{ feature_flag: { legacy_context_manager: true|false }}` -* Environment Variable: `NEW_RELIC_FEATURE_FLAG_LEGACY_CONTEXT_MANAGER` -* Description: The legacy context manager was replaced by AsyncLocalContextManager for async context propagation. If your application is not recording certain spans or creating orphaned data, you may want to enable this older context manager. Enabling this feature flag may increase the agent's use of memory and CPU. - #### kakfajs_instrumentation * Enabled by default: `false` * Configuration: `{ feature_flag: { kafkajs_instrumentation: true|false }}` diff --git a/documentation/nextjs/faqs/README.md b/documentation/nextjs/faqs/README.md new file mode 100644 index 0000000000..69088d6581 --- /dev/null +++ b/documentation/nextjs/faqs/README.md @@ -0,0 +1,8 @@ +# FAQs + +Are you having an issue with New Relic Next.js Instrumentation? Take a look at the following FAQS: + + * [Deploying Next.js to Cloud Provider](./cloud-providers.md) + * [Injecting New Relic Browser Agent](./browser-agent.md) + * [Instrumenting 3rd Party Libraries](./instrument-third-party-libraries.md) + * [Error Handling](./error-handling.md) diff --git a/documentation/nextjs/faqs/browser-agent.md b/documentation/nextjs/faqs/browser-agent.md new file mode 100644 index 0000000000..3e27b24013 --- /dev/null +++ b/documentation/nextjs/faqs/browser-agent.md @@ -0,0 +1,12 @@ +# Injecting Browser Agent + +Q: How can I inject the [New Relic Browser Agent](https://docs.newrelic.com/docs/browser/browser-monitoring/installation/install-browser-monitoring-agent/) into a Next.js project? + +A: It depends on if you are using the Pages or App Router for Next.js. + + +## Inject Browser Agent +The following links demonstrates how to inject the browser agent. + + * [Pages Router](https://github.com/newrelic/newrelic-node-examples/blob/e118117470ae9f9038c60d8a171a6f0d440f6291/nextjs-legacy/pages/_document.jsx) + * [App Router](https://github.com/newrelic/newrelic-node-examples/blob/58f760e828c45d90391bda3f66764d4420ba4990/nextjs-app-router/app/layout.js) diff --git a/documentation/nextjs/faqs/cloud-providers.md b/documentation/nextjs/faqs/cloud-providers.md new file mode 100644 index 0000000000..b2705c48a1 --- /dev/null +++ b/documentation/nextjs/faqs/cloud-providers.md @@ -0,0 +1,61 @@ +# Deploy Next.js to Cloud Provider + +Q: Can Next.js instrumentation work when deploying to [Vercel](https://vercel.com/frameworks/nextjs), [AWS Amplify](https://aws.amazon.com/amplify/), [Netlify](https://www.netlify.com/with/nextjs/), [Azure Static Sites](https://azure.microsoft.com/en-us/products/app-service/static), etc? + +A: The short answer is no. Most of these cloud providers lack the ability to control run options to load the New Relic Node.js agent. Also, most of these cloud providers execute code in a Function as a Service(FaaS) environment. Our agent requires a different setup and then additional processes to load the telemetry. Our recommendation is to rely on OpenTelemetry and load the telemetry via our OTLP endpoint. + +## OpenTelemetry setup with New Relic + +To setup Next.js to load OpenTelemetry data to New Relic you must do the following: + +1. Enable [experimental instrumentation hook](https://nextjs.org/docs/app/building-your-application/optimizing/open-telemetry). In your `next.config.js` add: + +```js +{ + experimental: { + instrumentationHook: true + } +} +``` + +2. Install OpenTelemetry packages. + +```sh +npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-http +``` + +3. Setup OpenTelemetry configuration in `new-relic-instrumentation.js` + +```js +const { NodeSDK } = require('@opentelemetry/sdk-node') +const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node') +const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http') +const { Resource } = require('@opentelemetry/resources') +const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions') +const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-node') + +const sdk = new NodeSDK({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'next-app', + }), + spanProcessor: new SimpleSpanProcessor(new OTLPTraceExporter({ + url: 'https://otlp.nr-data.net', + headers: { + 'api-key': process.env.NEW_RELIC_API_KEY + } + })), + instrumentations: [getNodeAutoInstrumentations()] +}) +sdk.start() +``` + +4. Add the following to `instrumentation.ts` in the root of your Next.js project: + +```js +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + require('./new-relic-instrumentation.js') + } +} +``` + diff --git a/documentation/nextjs/faqs/error-handling.md b/documentation/nextjs/faqs/error-handling.md new file mode 100644 index 0000000000..239e05c5ea --- /dev/null +++ b/documentation/nextjs/faqs/error-handling.md @@ -0,0 +1,15 @@ +# Injecting Browser Agent + +Q: How can I get the Next.js instrumentation to log errors to [New Relic Errors Inbox](https://docs.newrelic.com/docs/errors-inbox/errors-inbox/)? + +A: The Node.js agent has an API to log errors `newrelic.noticeError`. Next.js has an error page that can be used to add the API call. + + +## Log errors to Errors Inbox + +The error page varies between [Pages Router](https://nextjs.org/docs/pages/building-your-application/routing/custom-error) and [App Router](https://nextjs.org/docs/app/building-your-application/routing/error-handling) Next.js projects. + + + * [Pages Router](https://github.com/newrelic/newrelic-node-examples/blob/e118117470ae9f9038c60d8a171a6f0d440f6291/nextjs-legacy/pages/_error.jsx) error handling example. + + diff --git a/documentation/nextjs/faqs/instrument-third-party-libraries.md b/documentation/nextjs/faqs/instrument-third-party-libraries.md new file mode 100644 index 0000000000..818f737528 --- /dev/null +++ b/documentation/nextjs/faqs/instrument-third-party-libraries.md @@ -0,0 +1,23 @@ +# Instrument 3rd Party Libraries within Next.js + +Q: How can I get instrumentation to load for 3rd party libraries within my Next.js application like mysql, mongodb, pino, winston, etc? + +A: Typically the New Relic Node.js agent auto-instruments all supported [3rd party libraries](https://docs.newrelic.com/docs/apm/agents/nodejs-agent/getting-started/compatibility-requirements-nodejs-agent/#instrument). Next.js, however, bundles your project and code spilts between server and client side via webpack. To get auto-instrumentation to work, you must externalize all libraries within webpack. + +## Externalize 3rd party libraries in webpack + +To externalize all supported 3rd party libraries, add the following to `next.config.js`: + +```js +const nrExternals = require('newrelic/load-externals') + +module.exports = { + // In order for newrelic to effectively instrument a Next.js application, + // the modules that newrelic supports should not be mangled by webpack. Thus, + // we need to "externalize" all of the modules that newrelic supports. + webpack: (config) => { + nrExternals(config) + return config + } +} +``` diff --git a/documentation/nextjs/segments-and-spans.md b/documentation/nextjs/segments-and-spans.md new file mode 100644 index 0000000000..d0c56d4c13 --- /dev/null +++ b/documentation/nextjs/segments-and-spans.md @@ -0,0 +1,22 @@ +# Segments and spans + +Segments and spans (when distributed tracing is enabled) are captured for Next.js middleware and `getServerSideProps`(Server-Side Rendering). + +## Next.js middleware segments/spans + +[Next.js middleware](https://nextjs.org/docs/middleware) was made stable in 12.2.0. As of v0.2.0 of `@newrelic/next`, it will only instrument Next.js middleware in versions greater than or equal to 12.2.0. + +`/Nodejs/Middleware/Nextjs//middleware` + +Since middleware executes for every request you will see the same span for every request if middleware is present even if you aren't executing any business logic for a given route. If you have middleware in a deeply nested application, segments and spans will be created for every unique middleware. + +## Server-side rendering segments/spans + +`/Nodejs/Nextjs/getServerSideProps/` + +Next.js pages that contain server-side rendering must export a function called `getServerSideProps`. The function execution will be captured and an additional attribute will be added for the name of the page. + +**Attributes** +| Name | Description | +| --------- | ---------------------------------------------------------- | +| next.page | Name of the page, including dynamic route where applicable | diff --git a/documentation/nextjs/transactions.md b/documentation/nextjs/transactions.md new file mode 100644 index 0000000000..b91eae15b1 --- /dev/null +++ b/documentation/nextjs/transactions.md @@ -0,0 +1,39 @@ +# Transactions + +Transactions are captured as web transactions and named based on the Next.js page or API route. If you are using Next.js as a [custom server](https://nextjs.org/docs/advanced-features/custom-server), our Next.js instrumentation overrides the transaction naming of existing instrumentation for the custom server framework (for example, express, fastify, hapi, koa). Also, the transaction will be renamed based on the Next.js page or API route. + +Let's say we have a Next.js app with the following application structure: + +``` +pages + index.js + dynamic + static.js + [id].js +api + hiya.js + dynamic + [id].js +``` + +The transactions will be named as follows: + +| Request | Transaction Name | +| --------------------- | -------------------------------- | +| /pages/ | Nextjs/GET// | +| /pages/dynamic/static | Nextjs/GET//pages/dynamic/static | +| /pages/dynamic/example | Nextjs/GET//pages/dynamic/[id] | +| /api/hiya | Nextjs/GET//api/hiya | +| /api/dynamic/example | Nextjs/GET//api/dynamic/[id] | + + +## Errors +There are two exceptions to the transaction naming above. + +### 404s +If a request to a non-existent page or API route is made, the transaction name will flow through the Next.js 404 page and will be named as `Nextjs/GET//404`. + +### Non 404 errors +If a request is made that results in a 4xx or 5xx error, the transaction will flow through the Next.js error component and will be named as `Nextjs/GET//_error`. + + diff --git a/index.js b/index.js index c0ca0cb7a5..426442093c 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,6 @@ const featureFlags = require('./lib/feature_flags').prerelease const psemver = require('./lib/util/process-version') let logger = require('./lib/logger') // Gets re-loaded after initialization. const NAMES = require('./lib/metrics/names') -const isESMSupported = psemver.satisfies('>=16.2.0') const pkgJSON = require('./package.json') logger.info( @@ -246,15 +245,7 @@ function recordLoaderMetric(agent) { (arg === '--loader' || arg === '--experimental-loader') && process.execArgv[index + 1] === 'newrelic/esm-loader.mjs' ) { - if (isESMSupported) { - agent.metrics.getOrCreateMetric(NAMES.FEATURES.ESM.LOADER).incrementCallCount() - } else { - agent.metrics.getOrCreateMetric(NAMES.FEATURES.ESM.UNSUPPORTED_LOADER) - logger.warn( - 'New Relic for Node.js ESM loader requires a version of Node >= v16.12.0; your version is %s. Instrumentation will not be registered.', - process.version - ) - } + agent.metrics.getOrCreateMetric(NAMES.FEATURES.ESM.LOADER).incrementCallCount() } }) diff --git a/lib/context-manager/create-context-manager.js b/lib/context-manager/create-context-manager.js index 8c7d6cf710..336d79c1af 100644 --- a/lib/context-manager/create-context-manager.js +++ b/lib/context-manager/create-context-manager.js @@ -16,10 +16,6 @@ const logger = require('../logger') * the current configuration. */ function createContextManager(config) { - if (config.feature_flag.legacy_context_manager) { - return createLegacyContextManager(config) - } - return createAsyncLocalContextManager(config) } @@ -30,11 +26,4 @@ function createAsyncLocalContextManager(config) { return new AsyncLocalContextManager(config) } -function createLegacyContextManager(config) { - logger.info('Using LegacyContextManager') - - const LegacyContextManager = require('./legacy-context-manager') - return new LegacyContextManager(config) -} - module.exports = createContextManager diff --git a/lib/context-manager/legacy-context-manager.js b/lib/context-manager/legacy-context-manager.js deleted file mode 100644 index 7a42561039..0000000000 --- a/lib/context-manager/legacy-context-manager.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -/** - * Class for managing state in the agent. - * Keeps track of a single context instance. - * - * Given current usage with every instrumented function, the functions in this - * class should do as little work as possible to avoid unnecessary overhead. - * - * @class - */ -class LegacyContextManager { - /** - * @param {object} config New Relic config instance - */ - constructor(config) { - this._config = config - this._context = null - } - - /** - * Get the currently active context. - * - * @returns {object} The current active context. - */ - getContext() { - return this._context - } - - /** - * Set a new active context. Not bound to function execution. - * - * @param {object} newContext The context to set as active. - */ - setContext(newContext) { - this._context = newContext - } - - /** - * Run a function with the passed in context as the active context. - * Restores the previously active context upon completion. - * - * @param {object} context The context to set as active during callback execution. - * @param {Function} callback The function to execute in context. - * @param {Function} [cbThis] Optional `this` to apply to the callback. - * @param {Array<*>} [args] Optional arguments object or args array to invoke the callback with. - * @returns {*} Returns the value returned by the callback function. - */ - runInContext(context, callback, cbThis, args) { - const oldContext = this.getContext() - this.setContext(context) - - try { - return callback.apply(cbThis, args) - } finally { - this.setContext(oldContext) - } - } -} - -module.exports = LegacyContextManager diff --git a/lib/feature_flags.js b/lib/feature_flags.js index 590e34329d..974a4a4a7a 100644 --- a/lib/feature_flags.js +++ b/lib/feature_flags.js @@ -15,7 +15,6 @@ exports.prerelease = { reverse_naming_rules: false, undici_async_tracking: true, unresolved_promise_cleanup: true, - legacy_context_manager: false, kafkajs_instrumentation: false } @@ -43,4 +42,4 @@ exports.released = [ ] // flags that are no longer used for unreleased features -exports.unreleased = ['unreleased'] +exports.unreleased = ['unreleased', 'legacy_context_manager'] diff --git a/lib/instrumentation/core/async-hooks.js b/lib/instrumentation/core/async-hooks.js deleted file mode 100644 index 0b5bc9fb37..0000000000 --- a/lib/instrumentation/core/async-hooks.js +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const logger = require('../../logger').child({ component: 'async_hooks' }) -const asyncHooks = require('async_hooks') - -module.exports = initialize - -function initialize(agent, shim) { - if (!agent.config.feature_flag.legacy_context_manager) { - logger.debug( - 'New AsyncLocalStorage context enabled. Not enabling manual async_hooks or promise instrumentation' - ) - - return - } - - // this map is reused to track the segment that was active when - // the before callback is called to be replaced in the after callback - const segmentMap = new Map() - module.exports.segmentMap = segmentMap - - const hookHandlers = getHookHandlers(segmentMap, agent, shim) - maybeRegisterDestroyHook(segmentMap, agent, hookHandlers) - - const hook = asyncHooks.createHook(hookHandlers) - hook.enable() - - agent.on('unload', function disableHook() { - hook.disable() - }) - - return true -} - -/** - * Registers the async hooks events - * - * Note: The init only fires when the type is PROMISE. - * - * @param {Map} segmentMap map of async ids and segments - * @param {Agent} agent New Relic APM agent - * @param {Shim} shim instance of shim - * @returns {object} event handlers for async hooks - */ -function getHookHandlers(segmentMap, agent, shim) { - return { - init: function initHook(id, type, triggerId) { - if (type !== 'PROMISE') { - return - } - - const parentSegment = segmentMap.get(triggerId) - - if (parentSegment && !parentSegment.transaction.isActive()) { - // Stop propagating if the transaction was ended. - return - } - - if (!parentSegment && !agent.getTransaction()) { - return - } - - const activeSegment = shim.getActiveSegment() || parentSegment - - segmentMap.set(id, activeSegment) - }, - - before: function beforeHook(id) { - const hookSegment = segmentMap.get(id) - - if (!hookSegment) { - return - } - - segmentMap.set(id, shim.getActiveSegment()) - shim.setActiveSegment(hookSegment) - }, - after: function afterHook(id) { - const hookSegment = segmentMap.get(id) - - // hookSegment is the segment that was active before the promise - // executed. If the promise is executing before a segment has been - // restored, hookSegment will be null and should be restored. Thus - // undefined is the only invalid value here. - if (hookSegment === undefined) { - return - } - - segmentMap.set(id, shim.getActiveSegment()) - shim.setActiveSegment(hookSegment) - }, - promiseResolve: function promiseResolveHandler(id) { - const hookSegment = segmentMap.get(id) - segmentMap.delete(id) - - if (hookSegment === undefined) { - return - } - - // Because the ID will no-longer be in memory until dispose to propagate the null - // we need to set it active here or else we may continue to propagate the wrong tree. - // May be some risk of setting this at the wrong time - if (hookSegment === null) { - shim.setActiveSegment(hookSegment) - } - } - } -} - -/** - * Adds the destroy async hook event that will lean up any unresolved promises that have been destroyed. - * This defaults to true but does have a significant performance impact - * when customers have a lot of promises. - * See: https://github.com/newrelic/node-newrelic/issues/760 - * - * @param {Map} segmentMap map of async ids and segments - * @param {Agent} agent New Relic APM agent - * @param {object} hooks async-hook events - */ -function maybeRegisterDestroyHook(segmentMap, agent, hooks) { - if (agent.config.feature_flag.unresolved_promise_cleanup) { - logger.info('Adding destroy hook to clean up unresolved promises.') - hooks.destroy = function destroyHandler(id) { - segmentMap.delete(id) - } - } -} diff --git a/lib/instrumentation/core/globals.js b/lib/instrumentation/core/globals.js index da9174ca13..2cd4a2dfba 100644 --- a/lib/instrumentation/core/globals.js +++ b/lib/instrumentation/core/globals.js @@ -5,7 +5,6 @@ 'use strict' -const asyncHooks = require('./async-hooks') const symbols = require('../../symbols') module.exports = initialize @@ -63,8 +62,4 @@ function initialize(agent, nodule, name, shim) { return original.apply(this, arguments) } } - - // This will initialize the most optimal native-promise instrumentation that - // we have available. - asyncHooks(agent, shim) } diff --git a/lib/instrumentation/core/timers.js b/lib/instrumentation/core/timers.js index bd4f336179..1de2aa0d03 100644 --- a/lib/instrumentation/core/timers.js +++ b/lib/instrumentation/core/timers.js @@ -11,50 +11,10 @@ const Timers = require('timers') module.exports = initialize -function initialize(agent, timers, _moduleName, shim) { - const isLegacyContext = agent.config.feature_flag.legacy_context_manager - - if (isLegacyContext) { - instrumentProcessMethods(shim, process) - instrumentSetImmediate(shim, [timers, global]) - } - +function initialize(_agent, timers, _moduleName, shim) { instrumentTimerMethods(shim, [timers, global]) } -/** - * Sets up instrumentation for setImmediate on both timers and global. - * - * We do not want to create segments for setImmediate calls, - * as the object allocation may incur too much overhead in some situations - * - * @param {Shim} shim instance of shim - * @param {Array} pkgs array with references to timers and global - */ -function instrumentSetImmediate(shim, pkgs) { - pkgs.forEach((nodule) => { - if (shim.isWrapped(nodule.setImmediate)) { - return - } - - shim.wrap(nodule, 'setImmediate', function wrapSetImmediate(shim, fn) { - return function wrappedSetImmediate() { - const segment = shim.getActiveSegment() - if (!segment) { - return fn.apply(this, arguments) - } - - const args = shim.argsToArray.apply(shim, arguments, segment) - shim.bindSegment(args, shim.FIRST) - - return fn.apply(this, args) - } - }) - - copySymbols(shim, nodule, 'setImmediate') - }) -} - /** * Sets up instrumentation for setTimeout, setInterval and clearTimeout * on timers and global. @@ -107,37 +67,6 @@ function recordAsynchronizers(shim, _fn, name) { return new RecorderSpec({ name: 'timers.' + name, callback: shim.FIRST }) } -/** - * Instruments core process methods: nextTick, _nextDomainTick, _tickDomainCallback - * Note: This does not get registered when the context manager is async local - * - * @param {Shim} shim instance of shim - * @param {process} process global process object - */ -function instrumentProcessMethods(shim, process) { - const processMethods = ['nextTick', '_nextDomainTick', '_tickDomainCallback'] - - shim.wrap(process, processMethods, function wrapProcess(shim, fn) { - return function wrappedProcess() { - const segment = shim.getActiveSegment() - if (!segment) { - return fn.apply(this, arguments) - } - - // Manual copy because helper methods add significant overhead in some usages - const len = arguments.length - const args = new Array(len) - for (let i = 0; i < len; ++i) { - args[i] = arguments[i] - } - - shim.bindSegment(args, shim.FIRST, segment) - - return fn.apply(this, args) - } - }) -} - /** * Copies the symbols from original setTimeout and setInterval onto the wrapped functions * diff --git a/lib/instrumentation/director.js b/lib/instrumentation/director.js deleted file mode 100644 index 2320238e20..0000000000 --- a/lib/instrumentation/director.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const { MiddlewareSpec, MiddlewareMounterSpec } = require('../shim/specs') - -module.exports = function initialize(agent, director, moduleName, shim) { - shim.setFramework(shim.DIRECTOR) - - shim.setRouteParser(function routeParser(shim, fn, fnName, route) { - return route instanceof Array ? route.join('/') : route - }) - - const methods = ['on', 'route'] - const proto = director.Router.prototype - shim.wrapMiddlewareMounter( - proto, - methods, - new MiddlewareMounterSpec({ - route: shim.SECOND, - wrapper: function wrapMiddleware(shim, middleware, name, path) { - return shim.recordMiddleware( - middleware, - new MiddlewareSpec({ - route: path, - req: function getReq() { - return this.req - }, - params: function getParams() { - return this.params - }, - next: shim.LAST - }) - ) - } - }) - ) - - shim.wrap(proto, 'mount', function wrapMount(shim, mount) { - return function wrappedMount(routes, path) { - const isAsync = this.async - shim.wrap(routes, director.http.methods, function wrapRoute(shim, route) { - return shim.recordMiddleware( - route, - new MiddlewareSpec({ - route: path.join('/'), - req: function getReq() { - return this.req - }, - params: function getParams() { - return this.params - }, - next: isAsync ? shim.LAST : null - }) - ) - }) - const args = [routes, path] - return mount.apply(this, args) - } - }) -} diff --git a/lib/instrumentation/mongodb.js b/lib/instrumentation/mongodb.js index e8c201a289..1e82f33cff 100644 --- a/lib/instrumentation/mongodb.js +++ b/lib/instrumentation/mongodb.js @@ -6,8 +6,6 @@ 'use strict' const semver = require('semver') -const instrument = require('./mongodb/v2-mongo') -const instrumentV3 = require('./mongodb/v3-mongo') const instrumentV4 = require('./mongodb/v4-mongo') // XXX: When this instrumentation is modularized, update this thread @@ -34,14 +32,15 @@ function initialize(agent, mongodb, moduleName, shim) { return } - shim.setDatastore(shim.MONGODB) - const mongoVersion = shim.pkgVersion - if (semver.satisfies(mongoVersion, '>=4.0.0')) { - instrumentV4(shim, mongodb) - } else if (semver.satisfies(mongoVersion, '>=3.0.6')) { - instrumentV3(shim, mongodb) - } else { - instrument(shim, mongodb) + if (semver.satisfies(mongoVersion, '<4.0.0')) { + shim.logger.warn( + 'New Relic Node.js agent no longer supports mongodb < 4, current version %s. Please downgrade to v11 for support, if needed', + mongoVersion + ) + return } + + shim.setDatastore(shim.MONGODB) + instrumentV4(shim, mongodb) } diff --git a/lib/instrumentation/mongodb/v2-mongo.js b/lib/instrumentation/mongodb/v2-mongo.js deleted file mode 100644 index 5ee1231460..0000000000 --- a/lib/instrumentation/mongodb/v2-mongo.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const { captureAttributesOnStarted, makeQueryDescFunc } = require('./common') -const { OperationSpec } = require('../../shim/specs') - -/** - * parser used to grab the collection and operation - * from a running query - * - * @param {object} operation - */ -function queryParser(operation) { - let collection = this.collectionName || 'unknown' - if (this.ns) { - collection = this.ns.split(/\./)[1] || collection - } - - return { operation, collection } -} -/** - * Registers relevant instrumentation for mongo <= 3.0.6 - * and >= 2. This relies on the built-in "APM" hook points - * to instrument their provided objects as well as sets - * up a listener for when commands start to properly - * add necessary attributes to segments - * - * @param {Shim} shim - * @param {object} mongodb resolved package - */ -module.exports = function instrument(shim, mongodb) { - shim.setParser(queryParser) - - const recordDesc = { - Gridstore: { - isQuery: false, - makeDescFunc: function makeGridDesc(shim, fn, opName) { - return new OperationSpec({ name: 'GridFS-' + opName, callback: shim.LAST }) - } - }, - OrderedBulkOperation: { isQuery: true, makeDescFunc: makeQueryDescFunc }, - UnorderedBulkOperation: { isQuery: true, makeDescFunc: makeQueryDescFunc }, - CommandCursor: { isQuery: true, makeDescFunc: makeQueryDescFunc }, - AggregationCursor: { isQuery: true, makeDescFunc: makeQueryDescFunc }, - Cursor: { isQuery: true, makeDescFunc: makeQueryDescFunc }, - Collection: { isQuery: true, makeDescFunc: makeQueryDescFunc }, - Db: { - isQuery: false, - makeDescFunc: function makeDbDesc(shim, fn, method) { - return new OperationSpec({ callback: shim.LAST, name: method }) - } - } - } - - // instrument using the apm api - const instrumenter = mongodb.instrument(Object.create(null), instrumentModules) - captureAttributesOnStarted(shim, instrumenter) - - /** - * Every module groups instrumentations by their - * promise, callback, return permutations - * Iterate over permutations and properly - * wrap depending on the `recordDesc` above - * See: https://github.com/mongodb/node-mongodb-native/blob/v3.0.5/lib/collection.js#L384 - * - * @param _ - * @param modules - */ - function instrumentModules(_, modules) { - modules.forEach((module) => { - const { obj, instrumentations, name } = module - instrumentations.forEach((meta) => { - applyInstrumentation(name, obj, meta) - }) - }) - } - - /** - * Iterate over methods on object and lookup in `recordDesc` to decide - * if it needs to be wrapped as an operation or query - * - * @param {string} objectName name of class getting instrumented - * @param {object} object reference to the class getting instrumented - * @param {Define} meta describes the methods and if they are callbacks - * promises, and return values - */ - function applyInstrumentation(objectName, object, meta) { - const { methods, options } = meta - if (options.callback) { - methods.forEach((method) => { - const { isQuery, makeDescFunc } = recordDesc[objectName] - const proto = object.prototype - if (isQuery) { - shim.recordQuery(proto, method, makeDescFunc) - } else if (isQuery === false) { - // could be unset - shim.recordOperation(proto, method, makeDescFunc) - } else { - shim.logger.trace('No wrapping method found for %s', objectName) - } - }) - } - - // the cursor object implements Readable stream and internally calls nextObject on - // each read, in which case we do not want to record each nextObject() call - if (/Cursor$/.test(objectName)) { - shim.recordOperation( - object.prototype, - 'pipe', - new OperationSpec({ opaque: true, name: 'pipe' }) - ) - } - } -} diff --git a/lib/instrumentation/mongodb/v3-mongo.js b/lib/instrumentation/mongodb/v3-mongo.js deleted file mode 100644 index 14507b8722..0000000000 --- a/lib/instrumentation/mongodb/v3-mongo.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const { RecorderSpec } = require('../../shim/specs') -const { - captureAttributesOnStarted, - instrumentBulkOperation, - instrumentCollection, - instrumentCursor, - instrumentDb -} = require('./common') - -/** - * parser used to grab the collection and operation - * on every mongo operation - * - * @param {object} operation mongodb operation - * @returns {object} { operation, collection } parsed operation and collection - */ -function queryParser(operation) { - let collection = this.collectionName || 'unknown' - // in v3.3.0 aggregate commands added the collection - // to target - if (this.operation && this.operation.target) { - collection = this.operation.target - } else if (this.ns) { - collection = this.ns.split(/\./)[1] || collection - } else if (this.s && this.s.collection && this.s.collection.collectionName) { - collection = this.s.collection.collectionName - } - return { operation, collection } -} - -/** - * Records the `mongo.MongoClient.connect` operations. It also adds the first arg of connect(url) - * to a Symbol on the MongoClient to be used later to extract the host/port in cases where the topology - * is a cluster of domain sockets - * - * @param {Shim} shim instance of shim - * @param {object} mongodb resolved package - */ -function instrumentClient(shim, mongodb) { - shim.recordOperation(mongodb.MongoClient, 'connect', function wrappedConnect(shim) { - return new RecorderSpec({ callback: shim.LAST, name: 'connect' }) - }) -} - -/** - * Registers relevant instrumentation for mongo >= 3.0.6 - * In 3.0.6 they refactored their "APM" module which removed - * a lot of niceities around instrumentation classes. - * see: https://github.com/mongodb/node-mongodb-native/pull/1675/files - * This reverts back to instrumenting pre-canned methods on classes - * as well as sets up a listener for when commands start to properly - * add necessary attributes to segments - * - * @param {Shim} shim instance of shim - * @param {object} mongodb resolved package - */ -module.exports = function instrument(shim, mongodb) { - shim.setParser(queryParser) - instrumentClient(shim, mongodb) - const instrumenter = mongodb.instrument(Object.create(null), () => {}) - // in v3 of mongo endSessions fires after every command and it updates the active segment - // attributes with the admin database name which stomps on the database name where the original - // command runs on - captureAttributesOnStarted(shim, instrumenter, { skipCommands: ['endSessions'] }) - instrumentCursor(shim, mongodb.Cursor) - instrumentCursor(shim, shim.require('./lib/aggregation_cursor')) - instrumentCursor(shim, shim.require('./lib/command_cursor')) - instrumentBulkOperation(shim, shim.require('./lib/bulk/common')) - instrumentCollection(shim, mongodb.Collection) - instrumentDb(shim, mongodb.Db) - - // calling instrument sets up listeners for a few events - // we should restore this on unload to avoid leaking - // event emitters - shim.agent.once('unload', function uninstrumentMongo() { - instrumenter.uninstrument() - }) -} diff --git a/lib/instrumentation/nextjs/next-server.js b/lib/instrumentation/nextjs/next-server.js new file mode 100644 index 0000000000..c2528be5d6 --- /dev/null +++ b/lib/instrumentation/nextjs/next-server.js @@ -0,0 +1,178 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const semver = require('semver') +const { + assignCLMAttrs, + isMiddlewareInstrumentationSupported, + MIN_MW_SUPPORTED_VERSION, + MAX_MW_SUPPORTED_VERSION +} = require('./utils') +const { RecorderSpec } = require('../../shim/specs') +const SPAN_PREFIX = 'Nodejs/Nextjs' +const GET_SERVER_SIDE_PROP_VERSION = '13.4.5' + +module.exports = function initialize(shim, nextServer) { + const nextVersion = shim.require('./package.json').version + const { config } = shim.agent + shim.setFramework(shim.NEXT) + + const Server = nextServer.default + + shim.wrap( + Server.prototype, + 'renderToResponseWithComponents', + function wrapRenderToResponseWithComponents(shim, originalFn) { + return function wrappedRenderToResponseWithComponents() { + const [ctx, result] = arguments + const { pathname, renderOpts } = ctx + // this is not query params but instead url params for dynamic routes + const { query, components } = result + + if ( + semver.gte(nextVersion, GET_SERVER_SIDE_PROP_VERSION) && + components.getServerSideProps + ) { + shim.record(components, 'getServerSideProps', function recordGetServerSideProps() { + return new RecorderSpec({ + inContext(segment) { + segment.addSpanAttributes({ 'next.page': pathname }) + assignCLMAttrs(config, segment, { + 'code.function': 'getServerSideProps', + 'code.filepath': `pages${pathname}` + }) + }, + promise: true, + name: `${SPAN_PREFIX}/getServerSideProps/${pathname}` + }) + }) + } + + shim.setTransactionUri(pathname) + + const urlParams = extractRouteParams(ctx.query, renderOpts?.params || query) + assignParameters(shim, urlParams) + + return originalFn.apply(this, arguments) + } + } + ) + + shim.wrap(Server.prototype, 'runApi', function wrapRunApi(shim, originalFn) { + return function wrappedRunApi() { + const { page, params } = extractAttrs(arguments, nextVersion) + + shim.setTransactionUri(page) + + assignParameters(shim, params) + assignCLMAttrs(config, shim.getActiveSegment(), { + 'code.function': 'handler', + 'code.filepath': `pages${page}` + }) + + return originalFn.apply(this, arguments) + } + }) + + if (semver.lt(nextVersion, GET_SERVER_SIDE_PROP_VERSION)) { + shim.record( + Server.prototype, + 'renderHTML', + function renderHTMLRecorder(shim, renderToHTML, name, [, , page]) { + return new RecorderSpec({ + inContext(segment) { + segment.addSpanAttributes({ 'next.page': page }) + assignCLMAttrs(config, segment, { + 'code.function': 'getServerSideProps', + 'code.filepath': `pages${page}` + }) + }, + promise: true, + name: `${SPAN_PREFIX}/getServerSideProps/${page}` + }) + } + ) + } + + if (!isMiddlewareInstrumentationSupported(nextVersion)) { + shim.logger.warn( + `Next.js middleware instrumentation only supported on >=${MIN_MW_SUPPORTED_VERSION} <=${MAX_MW_SUPPORTED_VERSION}, got %s`, + nextVersion + ) + return + } + + shim.record(Server.prototype, 'runMiddleware', function runMiddlewareRecorder(shim) { + const middlewareName = 'middleware' + return new RecorderSpec({ + type: shim.MIDDLEWARE, + name: `${shim._metrics.MIDDLEWARE}${shim._metrics.PREFIX}/${middlewareName}`, + inContext(segment) { + assignCLMAttrs(config, segment, { + 'code.function': middlewareName, + 'code.filepath': middlewareName + }) + }, + promise: true + }) + }) +} + +function assignParameters(shim, parameters) { + const activeSegment = shim.getActiveSegment() + if (activeSegment) { + const transaction = activeSegment.transaction + + const prefixedParameters = shim.prefixRouteParameters(parameters) + + // We have to add params because this framework doesn't + // follow the traditional middleware/middleware mounter pattern + // where we'd pull these from middleware. + transaction.nameState.appendPath('/', prefixedParameters) + } +} + +/** + * Extracts the page and params from an API request + * + * @param {object} args arguments to runApi + * @param {string} version next.js version + * @returns {object} { page, params } + */ +function extractAttrs(args, version) { + let params + let page + if (semver.gte(version, '13.4.13')) { + const [, , , match] = args + page = match?.definition?.pathname + params = { ...match?.params } + } else { + ;[, , , params, page] = args + } + + return { params, page } +} + +/** + * Extracts route params from an object that contains both + * query and route params. The query params are automatically + * assigned when transaction finishes based on the url + * + * @param {object} query query params for given function call + * @param {object} params next.js params that contain query, route, and built in params + * @returns {object} route params + */ +function extractRouteParams(query = {}, params = {}) { + const queryParams = Object.keys(query) + const urlParams = {} + for (const [key, value] of Object.entries(params)) { + if (!queryParams.includes(key)) { + urlParams[key] = value + } + } + + return urlParams +} diff --git a/lib/instrumentation/nextjs/nr-hooks.js b/lib/instrumentation/nextjs/nr-hooks.js new file mode 100644 index 0000000000..d14c4932e9 --- /dev/null +++ b/lib/instrumentation/nextjs/nr-hooks.js @@ -0,0 +1,22 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const InstrumentationDescriptor = require('../../instrumentation-descriptor') + +// TODO: Remove once we update agent instrumentation to not rely on full required path within Node.js +// When running Next.js app as a standalone server this is how the next-server is getting loaded +module.exports = [ + { + type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK, + moduleName: 'next/dist/server/next-server', + onRequire: require('./next-server') + }, + { + type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK, + moduleName: './next-server', + onRequire: require('./next-server') + } +] diff --git a/lib/instrumentation/nextjs/utils.js b/lib/instrumentation/nextjs/utils.js new file mode 100644 index 0000000000..95c0645a32 --- /dev/null +++ b/lib/instrumentation/nextjs/utils.js @@ -0,0 +1,71 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const semver = require('semver') +const utils = module.exports + +/** + * Adds the relevant CLM attrs(code.function and code.filepath) to span if + * code_level_metrics.enabled is true and if span exists + * + * Note: This is not like the other in agent CLM support. Next.js is very rigid + * with its file structure and function names. We're providing relative paths to Next.js files + * based on the Next.js page. The function is also hardcoded to align with the conventions of Next.js. + * + * @param {Object} config agent config + * @param {TraceSegment} segment active segment to add CLM attrs to + * @param {Object} attrs list of CLM attrs to add to segment + */ +utils.assignCLMAttrs = function assignCLMAttrs(config, segment, attrs) { + // config is optionally accessed because agent could be older than + // when this configuration option was defined + if (!(config?.code_level_metrics?.enabled && segment)) { + return + } + + for (const [key, value] of Object.entries(attrs)) { + segment.addAttribute(key, value) + } +} + +// Version middleware is stable +// See: https://nextjs.org/docs/advanced-features/middleware +const MIN_MW_SUPPORTED_VERSION = '12.2.0' +// Middleware moved to worker thread +// We plan on revisiting when we release a stable version of our Next.js instrumentation +const MAX_MW_SUPPORTED_VERSION = '13.4.12' + +utils.MAX_MW_SUPPORTED_VERSION = MAX_MW_SUPPORTED_VERSION +utils.MIN_MW_SUPPORTED_VERSION = MIN_MW_SUPPORTED_VERSION +/** + * Middleware instrumentation has had quite the journey for us. + * As of 8/7/23 it no longer functions because it is running in a worker thread. + * Our instrumentation cannot propagate context in threads so for now we will no longer record this + * span. + * + * @param {string} version next.js version + * @returns {boolean} is middleware instrumentation supported + */ +utils.isMiddlewareInstrumentationSupported = function isMiddlewareInstrumentationSupported( + version +) { + return ( + semver.gte(version, MIN_MW_SUPPORTED_VERSION) && semver.lte(version, MAX_MW_SUPPORTED_VERSION) + ) +} + +/** + * Depending on the Next.js version the segment tree varies as it adds setTimeout segments. + * This util will find the segment that has `getServerSideProps` in the name + * + * @param {object} rootSegment trace root + * @returns {object} getServerSideProps segment + */ +utils.getServerSidePropsSegment = function getServerSidePropsSegment(rootSegment) { + return rootSegment.children[0].children.find((segment) => + segment.name.includes('getServerSideProps') + ) +} diff --git a/lib/instrumentation/redis.js b/lib/instrumentation/redis.js index 9e93b93ba5..c469bc9d41 100644 --- a/lib/instrumentation/redis.js +++ b/lib/instrumentation/redis.js @@ -5,7 +5,6 @@ 'use strict' -const hasOwnProperty = require('../util/properties').hasOwn const stringify = require('json-stringify-safe') const { OperationSpec, @@ -20,15 +19,19 @@ module.exports = function initialize(_agent, redis, _moduleName, shim) { shim.setDatastore(shim.REDIS) - if (proto.internal_send_command) { - registerInternalSendCommand(shim, proto) - } else { - registerSendCommand(shim, proto) + if (!proto.internal_send_command) { + shim.logger.warn( + 'New Relic Node.js agent no longer supports redis < 2.6.0, current version %s. Please downgrade to v11 for support, if needed', + shim.pkgVersion + ) + return } + + registerInternalSendCommand(shim, proto) } /** - * Instrumentation used in versions of redis > 2.6.1 < 4 to record all redis commands + * Instrumentation used in versions of redis >= 2.6.0 < 4 to record all redis commands * * @param {Shim} shim instance of shim * @param {object} proto RedisClient prototype @@ -40,7 +43,7 @@ function registerInternalSendCommand(shim, proto) { function wrapInternalSendCommand(shim, _, __, args) { const commandObject = args[0] const keys = commandObject.args - const parameters = getInstanceParameters(shim, this) + const parameters = getInstanceParameters(this) parameters.key = stringifyKeys(shim, keys) @@ -68,34 +71,6 @@ function registerInternalSendCommand(shim, proto) { ) } -/** - * Instrumentation used in versions of redis < 2.6.1 to record all redis commands - * - * @param {Shim} shim instance of shim - * @param {object} proto RedisClient prototype - */ -function registerSendCommand(shim, proto) { - shim.recordOperation(proto, 'send_command', function wrapSendCommand(shim, _, __, args) { - const [command, keys] = args - const parameters = getInstanceParameters(shim, this) - - parameters.key = stringifyKeys(shim, keys) - - return new OperationSpec({ - name: command || 'other', - parameters, - callback: function bindCallback(shim, _f, _n, segment) { - const last = args[args.length - 1] - if (shim.isFunction(last)) { - shim.bindCallbackSegment(null, args, shim.LAST, segment) - } else if (shim.isArray(last) && shim.isFunction(last[last.length - 1])) { - shim.bindCallbackSegment(null, last, shim.LAST, segment) - } - } - }) - }) -} - function stringifyKeys(shim, keys) { let key = null if (keys && keys.length && !shim.isFunction(keys)) { @@ -111,35 +86,14 @@ function stringifyKeys(shim, keys) { } /** - * Captures the necessary datastore parameters based on the specific version of redis + * Captures the necessary datastore parameters from redis client * - * @param {Shim} shim instance of shim * @param {object} client instance of redis client * @returns {object} datastore parameters */ -function getInstanceParameters(shim, client) { - if (hasOwnProperty(client, 'connection_options')) { - // for redis 2.4.0 - 2.6.2 - return doCapture(client, client.connection_options) - } else if (hasOwnProperty(client, 'connectionOption')) { - // for redis 0.12 - 2.2.5 - return doCapture(client, client.connectionOption) - } else if (hasOwnProperty(client, 'options')) { - // for redis 2.3.0 - 2.3.1 - return doCapture(client, client.options) - } - shim.logger.debug('Could not access instance attributes on connection.') - return doCapture() -} +function getInstanceParameters(client = {}) { + const opts = client?.connection_options -/** - * Extracts the relevant datastore parameters - * - * @param {object} client instance of redis client - * @param {object} opts options for the client instance - * @returns {object} datastore parameters - */ -function doCapture(client = {}, opts = {}) { return new DatastoreParameters({ host: opts.host || 'localhost', port_path_or_id: opts.path || opts.port || '6379', diff --git a/lib/instrumentations.js b/lib/instrumentations.js index 4b0f7f8d07..6e1be074c5 100644 --- a/lib/instrumentations.js +++ b/lib/instrumentations.js @@ -24,7 +24,6 @@ module.exports = function instrumentations() { 'bunyan': { type: InstrumentationDescriptor.TYPE_GENERIC }, 'cassandra-driver': { type: InstrumentationDescriptor.TYPE_DATASTORE }, 'connect': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK }, - 'director': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK }, 'express': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK }, 'fastify': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK }, 'generic-pool': { type: InstrumentationDescriptor.TYPE_GENERIC }, @@ -35,6 +34,7 @@ module.exports = function instrumentations() { 'memcached': { type: InstrumentationDescriptor.TYPE_DATASTORE }, 'mongodb': { type: InstrumentationDescriptor.TYPE_DATASTORE }, 'mysql': { module: './instrumentation/mysql' }, + 'next': { module: './instrumentation/nextjs' }, 'openai': { type: InstrumentationDescriptor.TYPE_GENERIC }, 'pg': { type: InstrumentationDescriptor.TYPE_DATASTORE }, 'pino': { module: './instrumentation/pino' }, diff --git a/lib/metrics/names.js b/lib/metrics/names.js index 6132a63431..b4b94e48ac 100644 --- a/lib/metrics/names.js +++ b/lib/metrics/names.js @@ -300,8 +300,7 @@ const INFINITE_TRACING = { const FEATURES = { ESM: { - LOADER: `${SUPPORTABILITY.FEATURES}/ESM/Loader`, - UNSUPPORTED_LOADER: `${SUPPORTABILITY.FEATURES}/ESM/UnsupportedLoader` + LOADER: `${SUPPORTABILITY.FEATURES}/ESM/Loader` }, CJS: { PRELOAD: `${SUPPORTABILITY.FEATURES}/CJS/Preload`, diff --git a/load-externals.js b/load-externals.js new file mode 100644 index 0000000000..6f947319be --- /dev/null +++ b/load-externals.js @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const instrumentedLibraries = require('./lib/instrumentations')() || {} +const libNames = Object.keys(instrumentedLibraries) +module.exports = function loadExternals(config) { + if (config.target.includes('node')) { + config.externals.push(...libNames) + } + + return config +} diff --git a/package.json b/package.json index b7dfd79105..9686cbbf33 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ ], "homepage": "https://github.com/newrelic/node-newrelic", "engines": { - "node": ">=16", + "node": ">=18", "npm": ">=6.0.0" }, "directories": { @@ -161,8 +161,8 @@ "bench": "node ./bin/run-bench.js", "docker-env": "./bin/docker-env-vars.sh", "docs": "rm -rf ./out && jsdoc -c ./jsdoc-conf.jsonc --private -r .", - "integration": "npm run prepare-test && npm run sub-install && time c8 -o ./coverage/integration tap --test-regex='(\\/|^test\\/integration\\/.*\\.tap\\.js)$' --timeout=600 --no-coverage --reporter classic", - "integration:esm": "time c8 -o ./coverage/integration-esm tap --node-arg='--loader=./esm-loader.mjs' --test-regex='(test\\/integration\\/.*\\.tap\\.mjs)$' --timeout=600 --no-coverage --reporter classic", + "integration": "npm run prepare-test && npm run sub-install && time c8 -o ./coverage/integration borp --timeout 600000 --reporter ./test/lib/test-reporter.mjs 'test/integration/**/*.tap.js'", + "integration:esm": "NODE_OPTIONS='--loader=./esm-loader.mjs' time c8 -o ./coverage/integration-esm borp --reporter ./test/lib/test-reporter.mjs 'test/integration/**/*.tap.mjs'", "prepare-test": "npm run ssl && npm run docker-env", "lint": "eslint ./*.{js,mjs} lib test bin examples", "lint:fix": "eslint --fix, ./*.{js,mjs} lib test bin examples", @@ -170,13 +170,13 @@ "publish-docs": "./bin/publish-docs.sh", "services": "DOCKER_PLATFORM=linux/$(uname -m) docker compose up -d --wait", "services:stop": "docker compose down", - "smoke": "npm run ssl && time tap test/smoke/**/**/*.tap.js --timeout=180 --no-coverage", + "smoke": "npm run ssl && time borp --timeout 180000 --reporter ./test/lib/test-reporter.mjs 'test/smoke/**/*.tap.js'", "ssl": "./bin/ssl.sh", "sub-install": "node test/bin/install_sub_deps", "test": "npm run integration && npm run unit", "third-party-updates": "oss third-party manifest --includeOptDeps && oss third-party notices --includeOptDeps && git add THIRD_PARTY_NOTICES.md third_party_manifest.json", - "unit": "rm -f newrelic_agent.log && time c8 -o ./coverage/unit tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.js)$' --timeout=180 --no-coverage --reporter classic", - "unit:scripts": "time c8 -o ./coverage/scripts-unit tap --test-regex='(\\/|^bin\\/test\\/.*\\.test\\.js)$' --timeout=180 --no-coverage --reporter classic", + "unit": "rm -f newrelic_agent.log && time c8 -o ./coverage/unit borp --timeout 180000 --reporter ./test/lib/test-reporter.mjs 'test/unit/**/*.test.js'", + "unit:scripts": "time c8 -o ./coverage/scripts-unit borp --reporter ./test/lib/test-reporter.mjs 'bin/test/*.test.js'", "update-cross-agent-tests": "./bin/update-cats.sh", "versioned-tests": "./bin/run-versioned-tests.sh", "update-changelog-version": "node ./bin/update-changelog-version", @@ -187,8 +187,6 @@ "versioned:external": "npm run checkout-external-versioned && SKIP_C8=true EXTERNAL_MODE=only time ./bin/run-versioned-tests.sh", "versioned:major": "VERSIONED_MODE=--major npm run versioned", "versioned": "npm run checkout-external-versioned && npm run prepare-test && time ./bin/run-versioned-tests.sh", - "versioned:legacy-context": "NEW_RELIC_FEATURE_FLAG_LEGACY_CONTEXT_MANAGER=1 npm run versioned", - "versioned:legacy-context:major": "NEW_RELIC_FEATURE_FLAG_LEGACY_CONTEXT_MANAGER=1 npm run versioned:major", "versioned:security": "NEW_RELIC_SECURITY_AGENT_ENABLED=true npm run versioned", "versioned:security:major": "NEW_RELIC_SECURITY_AGENT_ENABLED=true npm run versioned:major", "prepare": "husky install" @@ -214,7 +212,7 @@ }, "optionalDependencies": { "@contrast/fn-inspect": "^4.2.0", - "@newrelic/native-metrics": "^10.0.0", + "@newrelic/native-metrics": "^11.0.0", "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085" }, "devDependencies": { @@ -231,6 +229,7 @@ "ajv": "^6.12.6", "async": "^3.2.4", "aws-sdk": "^2.1604.0", + "borp": "^0.17.0", "c8": "^8.0.1", "clean-jsdoc-theme": "^4.2.18", "commander": "^7.0.0", @@ -272,6 +271,7 @@ "api.js", "stub_api.js", "newrelic.js", + "load-externals.js", "README.md", "LICENSE", "NEWS.md", diff --git a/test/integration/core/native-promises/async-hooks-new-promise-unresolved.tap.js b/test/integration/core/native-promises/async-hooks-new-promise-unresolved.tap.js deleted file mode 100644 index 4b149c8d13..0000000000 --- a/test/integration/core/native-promises/async-hooks-new-promise-unresolved.tap.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const exec = require('child_process').execSync -exec( - 'NEW_RELIC_FEATURE_FLAG_LEGACY_CONTEXT_MANAGER=1 NEW_RELIC_FEATURE_FLAG_UNRESOLVED_PROMISE_CLEANUP=false node --expose-gc ./async-hooks.js', - { - stdio: 'inherit', - cwd: __dirname - } -) diff --git a/test/integration/core/native-promises/async-hooks.js b/test/integration/core/native-promises/async-hooks.js deleted file mode 100644 index 949ac76ffc..0000000000 --- a/test/integration/core/native-promises/async-hooks.js +++ /dev/null @@ -1,623 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const test = require('tap').test -const helper = require('../../../lib/agent_helper') -const asyncHooks = require('async_hooks') - -test('await', function (t) { - const { agent } = setupAgent(t) - - helper.runInTransaction(agent, async function (txn) { - let transaction = agent.getTransaction() - t.equal(transaction && transaction.id, txn.id, 'should start in a transaction') - - const segmentMap = require('../../../../lib/instrumentation/core/async-hooks').segmentMap - - const promise = new Promise((resolve) => { - // don't immediately resolve so logic can kick in. - setImmediate(resolve) - }) - - // There may be extra promises in play - const promiseId = [...segmentMap.keys()].pop() - - await promise - - t.notOk(segmentMap.has(promiseId), 'should have removed segment for promise after resolve') - - transaction = agent.getTransaction() - t.equal( - transaction && transaction.id, - txn.id, - 'should resume in the same transaction after await' - ) - - txn.end() - - // Let the loop iterate to clear the microtask queue - setImmediate(() => { - t.equal(segmentMap.size, 0, 'should clear segments after all promises resolved') - t.end() - }) - }) -}) - -test("the agent's async hook", function (t) { - class TestResource extends asyncHooks.AsyncResource { - constructor(id) { - super('PROMISE', id) - } - - doStuff(callback) { - process.nextTick(() => { - if (this.runInAsyncScope) { - this.runInAsyncScope(callback) - } else { - this.emitBefore() - callback() - this.emitAfter() - } - }) - } - } - - t.autoend() - t.test('does not crash on multiple resolve calls', function (t) { - const { agent } = setupAgent(t) - helper.runInTransaction(agent, function () { - t.doesNotThrow(function () { - new Promise(function (resolve) { - resolve() - resolve() - }).then(t.end) - }) - }) - }) - - t.test('does not restore a segment for a resource created outside a transaction', function (t) { - const { agent, contextManager } = setupAgent(t) - - const testResource = new TestResource(1) - helper.runInTransaction(agent, function () { - const root = contextManager.getContext() - const segmentMap = require('../../../../lib/instrumentation/core/async-hooks').segmentMap - - t.equal(segmentMap.size, 0, 'no segments should be tracked') - testResource.doStuff(function () { - t.ok(contextManager.getContext(), 'should be in a transaction') - t.equal( - contextManager.getContext().name, - root.name, - 'loses transaction state for resources created outside of a transaction' - ) - t.end() - }) - }) - }) - - t.test('restores context in inactive transactions', function (t) { - const { agent, contextManager } = setupAgent(t) - - helper.runInTransaction(agent, function (txn) { - const testResource = new TestResource(1) - const root = contextManager.getContext() - txn.end() - testResource.doStuff(function () { - t.equal( - contextManager.getContext(), - root, - 'the hooks restore a segment when its transaction has been ended' - ) - t.end() - }) - }) - }) - - /** - * Represents same test as 'parent promises persist perspective to problematic progeny' - * from async_hooks.js. - * - * This specific use case is not currently supported with the implementation that clears - * segment references on promise resolve. - */ - t.test( - 'parent promises that are already resolved DO NOT persist to continuations ' + - 'scheduled after a timer async hop.', - function (t) { - const { agent } = setupAgent(t) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - const p = Promise.resolve() - - tasks.push(() => { - p.then(() => { - const tx = agent.getTransaction() - t.not( - tx ? tx.id : null, - txn.id, - 'If this failed, this use case now works! Time to switch to "t.equal"' - ) - t.end() - }) - }) - }) - } - ) - - /** - * Variation of 'parent promises persist perspective to problematic progeny' from async_hooks.js. - * - * For unresolved parent promises, persistance should stil work as expected. - */ - t.test('unresolved parent promises persist perspective to problematic progeny', function (t) { - const { agent } = setupAgent(t) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - let parentResolve = null - const p = new Promise((resolve) => { - parentResolve = resolve - }) - - tasks.push(() => { - p.then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - - t.end() - }) - - // resolve parent after continuation scheduled - parentResolve() - }) - }) - }) - - /** - * Represents same test as 'maintains transaction context' from async_hooks.js. - * - * Combination of a timer that does not propagate state and the new resolve - * mechanism that clears (and sets hook as active) causes this to fail. - */ - t.test('DOES NOT maintain transaction context', function (t) { - const { agent } = setupAgent(t) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - const segment = txn.trace.root - agent.tracer.bindFunction(one, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - - const wrapperThree = agent.tracer.bindFunction(function () { - return three() - }, segment) - - function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - } - - function executor(resolve) { - tasks.push(() => { - next().then(() => { - const tx = agent.getTransaction() - t.not( - tx ? tx.id : null, - txn.id, - 'If this failed, this use case now works! Time to switch to "t.equal"' - ) - resolve() - }) - }) - } - - function next() { - return Promise.resolve(wrapperTwo()) - } - - function two() { - return nextTwo() - } - - function nextTwo() { - return Promise.resolve(wrapperThree()) - } - - function three() {} - }) - }) - - t.test('maintains transaction context for unresolved promises', function (t) { - const { agent } = setupAgent(t) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - const segment = txn.trace.root - agent.tracer.bindFunction(function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - }, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - - const wrapperThree = agent.tracer.bindFunction(function () { - return three() - }, segment) - - function executor(resolve) { - setImmediate(() => { - next().then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - resolve() - }) - }) - } - - function next() { - return new Promise((resolve) => { - const val = wrapperTwo() - setImmediate(() => { - resolve(val) - }) - }) - } - - function two() { - return nextTwo() - } - - function nextTwo() { - return new Promise((resolve) => { - const val = wrapperThree() - setImmediate(() => { - resolve(val) - }) - }) - } - - function three() {} - }) - }) - - t.test('stops propagation on transaction end', function (t) { - const { agent, contextManager } = setupAgent(t) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - const segment = txn.trace.root - agent.tracer.bindFunction(one, segment)() - - function one() { - return new Promise((done) => { - const currentSegment = contextManager.getContext() - t.ok(currentSegment, 'should have propagated a segment') - txn.end() - - done() - }).then(() => { - const currentSegment = contextManager.getContext() - t.notOk(currentSegment, 'should not have a propagated segment') - t.end() - }) - } - }) - }) - - t.test('loses transaction context', function (t) { - const { agent } = setupAgent(t) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - const segment = txn.trace.root - agent.tracer.bindFunction(function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - }, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - - function executor(resolve) { - tasks.push(() => { - next().then(() => { - const tx = agent.getTransaction() - // We know tx will be null here because no promise was returned - // If this test fails, that's actually a good thing, - // so throw a party/update Koa. - t.equal(tx, null) - resolve() - }) - }) - } - - function next() { - return Promise.resolve(wrapperTwo()) - } - - function two() { - // No promise is returned to reinstate transaction context - } - }) - }) - - t.test('handles multientry callbacks correctly', function (t) { - const { agent, contextManager } = setupAgent(t) - - const segmentMap = require('../../../../lib/instrumentation/core/async-hooks').segmentMap - helper.runInTransaction(agent, function () { - const root = contextManager.getContext() - - const aSeg = agent.tracer.createSegment('A') - contextManager.setContext(aSeg) - const resA = new TestResource(1) - - const bSeg = agent.tracer.createSegment('B') - contextManager.setContext(bSeg) - const resB = new TestResource(2) - - contextManager.setContext(root) - - t.equal(segmentMap.size, 2, 'all resources should create an entry on init') - - resA.doStuff(() => { - t.equal( - contextManager.getContext().name, - aSeg.name, - 'runInAsyncScope should restore the segment active when a resource was made' - ) - - resB.doStuff(() => { - t.equal( - contextManager.getContext().name, - bSeg.name, - 'runInAsyncScope should restore the segment active when a resource was made' - ) - - t.end() - }) - t.equal( - contextManager.getContext().name, - aSeg.name, - 'runInAsyncScope should restore the segment active when a callback was called' - ) - }) - t.equal( - contextManager.getContext().name, - root.name, - 'root should be restored after we are finished' - ) - resA.doStuff(() => { - t.equal( - contextManager.getContext().name, - aSeg.name, - 'runInAsyncScope should restore the segment active when a resource was made' - ) - }) - }) - }) - - t.test( - 'cleans up unresolved promises on destroy', - { skip: process.env.NEW_RELIC_FEATURE_FLAG_UNRESOLVED_PROMISE_CLEANUP === 'false' }, - (t) => { - const { agent } = setupAgent(t) - const segmentMap = require('../../../../lib/instrumentation/core/async-hooks').segmentMap - - helper.runInTransaction(agent, () => { - /* eslint-disable no-unused-vars */ - let promise = unresolvedPromiseFunc() - - t.equal(segmentMap.size, 1, 'segment map should have 1 element') - - promise = null - - global.gc && global.gc() - - setImmediate(() => { - t.equal(segmentMap.size, 0, 'segment map should clean up unresolved promises on destroy') - t.end() - }) - }) - - function unresolvedPromiseFunc() { - return new Promise(() => {}) - } - } - ) - - t.test( - 'does not clean up unresolved promises on destroy when `unresolved_promise_cleanup` is set to false', - { skip: process.env.NEW_RELIC_FEATURE_FLAG_UNRESOLVED_PROMISE_CLEANUP !== 'false' }, - (t) => { - const { agent } = setupAgent(t) - const segmentMap = require('../../../../lib/instrumentation/core/async-hooks').segmentMap - - helper.runInTransaction(agent, () => { - /* eslint-disable no-unused-vars */ - let promise = unresolvedPromiseFunc() - - t.equal(segmentMap.size, 1, 'segment map should have 1 element') - - promise = null - - global.gc && global.gc() - - setImmediate(() => { - t.equal( - segmentMap.size, - 1, - 'segment map should not clean up unresolved promise on destroy' - ) - t.end() - }) - }) - - function unresolvedPromiseFunc() { - return new Promise(() => {}) - } - } - ) -}) - -function checkCallMetrics(t, testMetrics) { - // Tap also creates promises, so these counts don't quite match the tests. - const TAP_COUNT = 1 - - t.equal(testMetrics.initCalled - TAP_COUNT, 2, 'two promises were created') - t.equal(testMetrics.beforeCalled, 1, 'before hook called for all async promises') - t.equal( - testMetrics.beforeCalled, - testMetrics.afterCalled, - 'before should be called as many times as after' - ) - - if (global.gc) { - global.gc() - return setTimeout(function () { - t.equal( - testMetrics.initCalled - TAP_COUNT, - testMetrics.destroyCalled, - 'all promises created were destroyed' - ) - t.end() - }, 10) - } - t.end() -} - -test('promise hooks', function (t) { - t.autoend() - const testMetrics = { - initCalled: 0, - beforeCalled: 0, - afterCalled: 0, - destroyCalled: 0 - } - - const promiseIds = {} - const hook = asyncHooks.createHook({ - init: function initHook(id, type) { - if (type === 'PROMISE') { - promiseIds[id] = true - testMetrics.initCalled++ - } - }, - before: function beforeHook(id) { - if (promiseIds[id]) { - testMetrics.beforeCalled++ - } - }, - after: function afterHook(id) { - if (promiseIds[id]) { - testMetrics.afterCalled++ - } - }, - destroy: function destHook(id) { - if (promiseIds[id]) { - testMetrics.destroyCalled++ - } - } - }) - hook.enable() - - t.test('are only called once during the lifetime of a promise', function (t) { - new Promise(function (resolve) { - setTimeout(resolve, 10) - }).then(function () { - setImmediate(checkCallMetrics, t, testMetrics) - }) - }) -}) - -function setupAgent(t) { - const agent = helper.instrumentMockedAgent({ - feature_flag: { - await_support: true - } - }) - - const contextManager = helper.getContextManager() - - t.teardown(function () { - helper.unloadAgent(agent) - }) - - return { - agent, - contextManager - } -} diff --git a/test/integration/core/native-promises/async-hooks.tap.js b/test/integration/core/native-promises/async-hooks.tap.js deleted file mode 100644 index 05414f3e76..0000000000 --- a/test/integration/core/native-promises/async-hooks.tap.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const exec = require('child_process').execSync -exec('NEW_RELIC_FEATURE_FLAG_LEGACY_CONTEXT_MANAGER=1 node --expose-gc ./async-hooks.js', { - stdio: 'inherit', - cwd: __dirname -}) diff --git a/test/integration/core/native-promises/native-promises.js b/test/integration/core/native-promises/native-promises.js deleted file mode 100644 index dc2d55d609..0000000000 --- a/test/integration/core/native-promises/native-promises.js +++ /dev/null @@ -1,636 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const { test } = require('tap') - -const helper = require('../../../lib/agent_helper') -const asyncHooks = require('async_hooks') - -test('AsyncLocalStorage based tracking', (t) => { - t.autoend() - - const config = {} - - createPromiseTests(t, config) - - // Negative assertion case mirroring test for async-hooks. - // This is a new limitation due to AsyncLocalStorage propagation only on init. - // The timer-hop without context prior to .then() continuation causes the state loss. - t.test('DOES NOT maintain transaction context over contextless timer', (t) => { - const { agent } = setupAgent(t, config) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - const segment = txn.trace.root - agent.tracer.bindFunction(one, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - const wrapperThree = agent.tracer.bindFunction(function () { - return three() - }, segment) - - function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - } - - function executor(resolve) { - tasks.push(() => { - next().then(() => { - const tx = agent.getTransaction() - t.notOk( - tx, - 'If fails, we have fixed a limitation and should check equal transaction IDs' - ) - resolve() - }) - }) - } - - function next() { - return Promise.resolve(wrapperTwo()) - } - - function two() { - return nextTwo() - } - - function nextTwo() { - return Promise.resolve(wrapperThree()) - } - - function three() {} - }) - }) - - // Negative assertion case mirroring test for async-hooks. - // This is a new limitation due to AsyncLocalStorage propagation only on init. - // The timer-hop without context prior to .then() continuation causes the state loss. - t.test('parent promises DO NOT persist perspective to problematic progeny', (t) => { - const { agent } = setupAgent(t, config) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - const p = Promise.resolve() - - tasks.push(() => { - p.then(() => { - const tx = agent.getTransaction() - - t.notOk(tx, 'If fails, we have fixed a limitation and should check equal transaction IDs') - t.end() - }) - }) - }) - }) - - // Negative assertion case mirroring test for async-hooks. - // This is a new limitation due to AsyncLocalStorage propagation only on init. - // The timer-hop without context prior to .then() continuation causes the state loss. - t.test('unresolved parent promises DO NOT persist to problematic progeny', (t) => { - const { agent } = setupAgent(t, config) - const tasks = [] - const intervalId = setInterval(() => { - while (tasks.length) { - tasks.pop()() - } - }, 10) - - t.teardown(() => { - clearInterval(intervalId) - }) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - let parentResolve = null - const p = new Promise((resolve) => { - parentResolve = resolve - }) - - tasks.push(() => { - p.then(() => { - const tx = agent.getTransaction() - t.notOk(tx, 'If fails, we have fixed a limitation and should check equal transaction IDs') - - t.end() - }) - - // resolve parent after continuation scheduled - parentResolve() - }) - }) - }) -}) - -function createPromiseTests(t, config) { - t.test('maintains context across await', function (t) { - const { agent } = setupAgent(t, config) - helper.runInTransaction(agent, async function (txn) { - let transaction = agent.getTransaction() - t.equal(transaction && transaction.id, txn.id, 'should start in a transaction') - - await Promise.resolve("i'll be back") - - transaction = agent.getTransaction() - t.equal( - transaction && transaction.id, - txn.id, - 'should resume in the same transaction after await' - ) - - txn.end() - t.end() - }) - }) - - t.test('maintains context across multiple awaits', async (t) => { - const { agent } = setupAgent(t, config) - await helper.runInTransaction(agent, async function (createdTransaction) { - let transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id, 'should start in a transaction') - - await firstFunction() - transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id) - - await secondFunction() - transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id) - - createdTransaction.end() - - async function firstFunction() { - await childFunction() - - transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id) - } - - async function childFunction() { - await new Promise((resolve) => { - transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id) - - setTimeout(resolve, 1) - }) - } - - async function secondFunction() { - await new Promise((resolve) => { - setImmediate(resolve) - }) - } - }) - }) - - t.test('maintains context across promise chain', (t) => { - const { agent } = setupAgent(t, config) - helper.runInTransaction(agent, function (createdTransaction) { - let transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id, 'should start in a transaction') - firstFunction() - .then(() => { - transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id) - return secondFunction() - }) - .then(() => { - transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id) - createdTransaction.end() - t.end() - }) - - function firstFunction() { - return childFunction() - } - - function childFunction() { - return new Promise((resolve) => { - transaction = agent.getTransaction() - t.equal(transaction && transaction.id, createdTransaction.id) - - setTimeout(resolve, 1) - }) - } - - function secondFunction() { - return new Promise((resolve) => { - setImmediate(resolve) - }) - } - }) - }) - - t.test('does not crash on multiple resolve calls', function (t) { - const { agent } = setupAgent(t, config) - helper.runInTransaction(agent, function () { - t.doesNotThrow(function () { - new Promise(function (res) { - res() - res() - }).then(t.end) - }) - }) - }) - - t.test('restores context in inactive transactions', function (t) { - const { agent, contextManager } = setupAgent(t, config) - - helper.runInTransaction(agent, function (txn) { - const res = new TestResource(1) - const root = contextManager.getContext() - txn.end() - res.doStuff(function () { - t.equal( - contextManager.getContext(), - root, - 'should restore a segment when its transaction has been ended' - ) - t.end() - }) - }) - }) - - t.test('handles multi-entry callbacks correctly', function (t) { - const { agent, contextManager } = setupAgent(t, config) - - helper.runInTransaction(agent, function () { - const root = contextManager.getContext() - - const aSeg = agent.tracer.createSegment('A') - contextManager.setContext(aSeg) - - const resA = new TestResource(1) - - const bSeg = agent.tracer.createSegment('B') - contextManager.setContext(bSeg) - const resB = new TestResource(2) - - contextManager.setContext(root) - - resA.doStuff(() => { - t.equal( - contextManager.getContext().name, - aSeg.name, - 'runInAsyncScope should restore the segment active when a resource was made' - ) - - resB.doStuff(() => { - t.equal( - contextManager.getContext().name, - bSeg.name, - 'runInAsyncScope should restore the segment active when a resource was made' - ) - - t.end() - }) - t.equal( - contextManager.getContext().name, - aSeg.name, - 'runInAsyncScope should restore the segment active when a callback was called' - ) - }) - t.equal( - contextManager.getContext().name, - root.name, - 'root should be restored after we are finished' - ) - resA.doStuff(() => { - t.equal( - contextManager.getContext().name, - aSeg.name, - 'runInAsyncScope should restore the segment active when a resource was made' - ) - }) - }) - }) - - t.test('maintains transaction context over setImmediate in-context', (t) => { - const { agent } = setupAgent(t, config) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - const segment = txn.trace.root - agent.tracer.bindFunction(function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - }, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - const wrapperThree = agent.tracer.bindFunction(function () { - return three() - }, segment) - - function executor(resolve) { - setImmediate(() => { - next().then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - resolve() - }) - }) - } - - function next() { - return Promise.resolve(wrapperTwo()) - } - - function two() { - return nextTwo() - } - - function nextTwo() { - return Promise.resolve(wrapperThree()) - } - - function three() {} - }) - }) - - t.test('maintains transaction context over process.nextTick in-context', (t) => { - const { agent } = setupAgent(t, config) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - const segment = txn.trace.root - agent.tracer.bindFunction(function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - }, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - const wrapperThree = agent.tracer.bindFunction(function () { - return three() - }, segment) - - function executor(resolve) { - process.nextTick(() => { - next().then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - resolve() - }) - }) - } - - function next() { - return Promise.resolve(wrapperTwo()) - } - - function two() { - return nextTwo() - } - - function nextTwo() { - return Promise.resolve(wrapperThree()) - } - - function three() {} - }) - }) - - t.test('maintains transaction context over setTimeout in-context', (t) => { - const { agent } = setupAgent(t, config) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - const segment = txn.trace.root - agent.tracer.bindFunction(function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - }, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - const wrapperThree = agent.tracer.bindFunction(function () { - return three() - }, segment) - - function executor(resolve) { - setTimeout(() => { - next().then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - resolve() - }) - }, 1) - } - - function next() { - return Promise.resolve(wrapperTwo()) - } - - function two() { - return nextTwo() - } - - function nextTwo() { - return Promise.resolve(wrapperThree()) - } - - function three() {} - }) - }) - - t.test('maintains transaction context over setInterval in-context', (t) => { - const { agent } = setupAgent(t, config) - - helper.runInTransaction(agent, function (txn) { - t.ok(txn, 'transaction should not be null') - - const segment = txn.trace.root - agent.tracer.bindFunction(function one() { - return new Promise(executor).then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - t.end() - }) - }, segment)() - - const wrapperTwo = agent.tracer.bindFunction(function () { - return two() - }, segment) - const wrapperThree = agent.tracer.bindFunction(function () { - return three() - }, segment) - - function executor(resolve) { - const ref = setInterval(() => { - clearInterval(ref) - - next().then(() => { - const tx = agent.getTransaction() - t.equal(tx ? tx.id : null, txn.id) - resolve() - }) - }, 1) - } - - function next() { - return Promise.resolve(wrapperTwo()) - } - - function two() { - return nextTwo() - } - - function nextTwo() { - return Promise.resolve(wrapperThree()) - } - - function three() {} - }) - }) -} - -function checkCallMetrics(t, testMetrics) { - // Tap also creates promises, so these counts don't quite match the tests. - const TAP_COUNT = 1 - - t.equal(testMetrics.initCalled - TAP_COUNT, 2, 'two promises were created') - t.equal(testMetrics.beforeCalled, 1, 'before hook called for all async promises') - t.equal( - testMetrics.beforeCalled, - testMetrics.afterCalled, - 'before should be called as many times as after' - ) - - if (global.gc) { - global.gc() - return setTimeout(function () { - t.equal( - testMetrics.initCalled - TAP_COUNT, - testMetrics.destroyCalled, - 'all promises created were destroyed' - ) - t.end() - }, 10) - } - t.end() -} - -test('promise hooks', function (t) { - t.autoend() - const testMetrics = { - initCalled: 0, - beforeCalled: 0, - afterCalled: 0, - destroyCalled: 0 - } - - const promiseIds = {} - const hook = asyncHooks.createHook({ - init: function initHook(id, type) { - if (type === 'PROMISE') { - promiseIds[id] = true - testMetrics.initCalled++ - } - }, - before: function beforeHook(id) { - if (promiseIds[id]) { - testMetrics.beforeCalled++ - } - }, - after: function afterHook(id) { - if (promiseIds[id]) { - testMetrics.afterCalled++ - } - }, - destroy: function destHook(id) { - if (promiseIds[id]) { - testMetrics.destroyCalled++ - } - } - }) - hook.enable() - - t.test('are only called once during the lifetime of a promise', function (t) { - new Promise(function (res) { - setTimeout(res, 10) - }).then(function () { - setImmediate(checkCallMetrics, t, testMetrics) - }) - }) -}) - -function setupAgent(t, config) { - const agent = helper.instrumentMockedAgent(config) - t.teardown(function () { - helper.unloadAgent(agent) - }) - - const contextManager = helper.getContextManager() - - return { - agent, - contextManager - } -} - -class TestResource extends asyncHooks.AsyncResource { - constructor(id) { - super('PROMISE', id) - } - - doStuff(callback) { - process.nextTick(() => { - if (this.runInAsyncScope) { - this.runInAsyncScope(callback) - } else { - this.emitBefore() - callback() - this.emitAfter() - } - }) - } -} diff --git a/test/integration/core/native-promises/native-promises.tap.js b/test/integration/core/native-promises/native-promises.tap.js index 3297f1fafb..dc2d55d609 100644 --- a/test/integration/core/native-promises/native-promises.tap.js +++ b/test/integration/core/native-promises/native-promises.tap.js @@ -5,8 +5,632 @@ 'use strict' -const exec = require('child_process').execSync -exec('node --expose-gc ./native-promises.js', { - stdio: 'inherit', - cwd: __dirname +const { test } = require('tap') + +const helper = require('../../../lib/agent_helper') +const asyncHooks = require('async_hooks') + +test('AsyncLocalStorage based tracking', (t) => { + t.autoend() + + const config = {} + + createPromiseTests(t, config) + + // Negative assertion case mirroring test for async-hooks. + // This is a new limitation due to AsyncLocalStorage propagation only on init. + // The timer-hop without context prior to .then() continuation causes the state loss. + t.test('DOES NOT maintain transaction context over contextless timer', (t) => { + const { agent } = setupAgent(t, config) + const tasks = [] + const intervalId = setInterval(() => { + while (tasks.length) { + tasks.pop()() + } + }, 10) + + t.teardown(() => { + clearInterval(intervalId) + }) + + helper.runInTransaction(agent, function (txn) { + t.ok(txn, 'transaction should not be null') + + const segment = txn.trace.root + agent.tracer.bindFunction(one, segment)() + + const wrapperTwo = agent.tracer.bindFunction(function () { + return two() + }, segment) + const wrapperThree = agent.tracer.bindFunction(function () { + return three() + }, segment) + + function one() { + return new Promise(executor).then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + t.end() + }) + } + + function executor(resolve) { + tasks.push(() => { + next().then(() => { + const tx = agent.getTransaction() + t.notOk( + tx, + 'If fails, we have fixed a limitation and should check equal transaction IDs' + ) + resolve() + }) + }) + } + + function next() { + return Promise.resolve(wrapperTwo()) + } + + function two() { + return nextTwo() + } + + function nextTwo() { + return Promise.resolve(wrapperThree()) + } + + function three() {} + }) + }) + + // Negative assertion case mirroring test for async-hooks. + // This is a new limitation due to AsyncLocalStorage propagation only on init. + // The timer-hop without context prior to .then() continuation causes the state loss. + t.test('parent promises DO NOT persist perspective to problematic progeny', (t) => { + const { agent } = setupAgent(t, config) + const tasks = [] + const intervalId = setInterval(() => { + while (tasks.length) { + tasks.pop()() + } + }, 10) + + t.teardown(() => { + clearInterval(intervalId) + }) + + helper.runInTransaction(agent, function (txn) { + t.ok(txn, 'transaction should not be null') + + const p = Promise.resolve() + + tasks.push(() => { + p.then(() => { + const tx = agent.getTransaction() + + t.notOk(tx, 'If fails, we have fixed a limitation and should check equal transaction IDs') + t.end() + }) + }) + }) + }) + + // Negative assertion case mirroring test for async-hooks. + // This is a new limitation due to AsyncLocalStorage propagation only on init. + // The timer-hop without context prior to .then() continuation causes the state loss. + t.test('unresolved parent promises DO NOT persist to problematic progeny', (t) => { + const { agent } = setupAgent(t, config) + const tasks = [] + const intervalId = setInterval(() => { + while (tasks.length) { + tasks.pop()() + } + }, 10) + + t.teardown(() => { + clearInterval(intervalId) + }) + + helper.runInTransaction(agent, function (txn) { + t.ok(txn, 'transaction should not be null') + + let parentResolve = null + const p = new Promise((resolve) => { + parentResolve = resolve + }) + + tasks.push(() => { + p.then(() => { + const tx = agent.getTransaction() + t.notOk(tx, 'If fails, we have fixed a limitation and should check equal transaction IDs') + + t.end() + }) + + // resolve parent after continuation scheduled + parentResolve() + }) + }) + }) }) + +function createPromiseTests(t, config) { + t.test('maintains context across await', function (t) { + const { agent } = setupAgent(t, config) + helper.runInTransaction(agent, async function (txn) { + let transaction = agent.getTransaction() + t.equal(transaction && transaction.id, txn.id, 'should start in a transaction') + + await Promise.resolve("i'll be back") + + transaction = agent.getTransaction() + t.equal( + transaction && transaction.id, + txn.id, + 'should resume in the same transaction after await' + ) + + txn.end() + t.end() + }) + }) + + t.test('maintains context across multiple awaits', async (t) => { + const { agent } = setupAgent(t, config) + await helper.runInTransaction(agent, async function (createdTransaction) { + let transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id, 'should start in a transaction') + + await firstFunction() + transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id) + + await secondFunction() + transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id) + + createdTransaction.end() + + async function firstFunction() { + await childFunction() + + transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id) + } + + async function childFunction() { + await new Promise((resolve) => { + transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id) + + setTimeout(resolve, 1) + }) + } + + async function secondFunction() { + await new Promise((resolve) => { + setImmediate(resolve) + }) + } + }) + }) + + t.test('maintains context across promise chain', (t) => { + const { agent } = setupAgent(t, config) + helper.runInTransaction(agent, function (createdTransaction) { + let transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id, 'should start in a transaction') + firstFunction() + .then(() => { + transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id) + return secondFunction() + }) + .then(() => { + transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id) + createdTransaction.end() + t.end() + }) + + function firstFunction() { + return childFunction() + } + + function childFunction() { + return new Promise((resolve) => { + transaction = agent.getTransaction() + t.equal(transaction && transaction.id, createdTransaction.id) + + setTimeout(resolve, 1) + }) + } + + function secondFunction() { + return new Promise((resolve) => { + setImmediate(resolve) + }) + } + }) + }) + + t.test('does not crash on multiple resolve calls', function (t) { + const { agent } = setupAgent(t, config) + helper.runInTransaction(agent, function () { + t.doesNotThrow(function () { + new Promise(function (res) { + res() + res() + }).then(t.end) + }) + }) + }) + + t.test('restores context in inactive transactions', function (t) { + const { agent, contextManager } = setupAgent(t, config) + + helper.runInTransaction(agent, function (txn) { + const res = new TestResource(1) + const root = contextManager.getContext() + txn.end() + res.doStuff(function () { + t.equal( + contextManager.getContext(), + root, + 'should restore a segment when its transaction has been ended' + ) + t.end() + }) + }) + }) + + t.test('handles multi-entry callbacks correctly', function (t) { + const { agent, contextManager } = setupAgent(t, config) + + helper.runInTransaction(agent, function () { + const root = contextManager.getContext() + + const aSeg = agent.tracer.createSegment('A') + contextManager.setContext(aSeg) + + const resA = new TestResource(1) + + const bSeg = agent.tracer.createSegment('B') + contextManager.setContext(bSeg) + const resB = new TestResource(2) + + contextManager.setContext(root) + + resA.doStuff(() => { + t.equal( + contextManager.getContext().name, + aSeg.name, + 'runInAsyncScope should restore the segment active when a resource was made' + ) + + resB.doStuff(() => { + t.equal( + contextManager.getContext().name, + bSeg.name, + 'runInAsyncScope should restore the segment active when a resource was made' + ) + + t.end() + }) + t.equal( + contextManager.getContext().name, + aSeg.name, + 'runInAsyncScope should restore the segment active when a callback was called' + ) + }) + t.equal( + contextManager.getContext().name, + root.name, + 'root should be restored after we are finished' + ) + resA.doStuff(() => { + t.equal( + contextManager.getContext().name, + aSeg.name, + 'runInAsyncScope should restore the segment active when a resource was made' + ) + }) + }) + }) + + t.test('maintains transaction context over setImmediate in-context', (t) => { + const { agent } = setupAgent(t, config) + + helper.runInTransaction(agent, function (txn) { + t.ok(txn, 'transaction should not be null') + + const segment = txn.trace.root + agent.tracer.bindFunction(function one() { + return new Promise(executor).then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + t.end() + }) + }, segment)() + + const wrapperTwo = agent.tracer.bindFunction(function () { + return two() + }, segment) + const wrapperThree = agent.tracer.bindFunction(function () { + return three() + }, segment) + + function executor(resolve) { + setImmediate(() => { + next().then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + resolve() + }) + }) + } + + function next() { + return Promise.resolve(wrapperTwo()) + } + + function two() { + return nextTwo() + } + + function nextTwo() { + return Promise.resolve(wrapperThree()) + } + + function three() {} + }) + }) + + t.test('maintains transaction context over process.nextTick in-context', (t) => { + const { agent } = setupAgent(t, config) + + helper.runInTransaction(agent, function (txn) { + t.ok(txn, 'transaction should not be null') + + const segment = txn.trace.root + agent.tracer.bindFunction(function one() { + return new Promise(executor).then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + t.end() + }) + }, segment)() + + const wrapperTwo = agent.tracer.bindFunction(function () { + return two() + }, segment) + const wrapperThree = agent.tracer.bindFunction(function () { + return three() + }, segment) + + function executor(resolve) { + process.nextTick(() => { + next().then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + resolve() + }) + }) + } + + function next() { + return Promise.resolve(wrapperTwo()) + } + + function two() { + return nextTwo() + } + + function nextTwo() { + return Promise.resolve(wrapperThree()) + } + + function three() {} + }) + }) + + t.test('maintains transaction context over setTimeout in-context', (t) => { + const { agent } = setupAgent(t, config) + + helper.runInTransaction(agent, function (txn) { + t.ok(txn, 'transaction should not be null') + + const segment = txn.trace.root + agent.tracer.bindFunction(function one() { + return new Promise(executor).then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + t.end() + }) + }, segment)() + + const wrapperTwo = agent.tracer.bindFunction(function () { + return two() + }, segment) + const wrapperThree = agent.tracer.bindFunction(function () { + return three() + }, segment) + + function executor(resolve) { + setTimeout(() => { + next().then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + resolve() + }) + }, 1) + } + + function next() { + return Promise.resolve(wrapperTwo()) + } + + function two() { + return nextTwo() + } + + function nextTwo() { + return Promise.resolve(wrapperThree()) + } + + function three() {} + }) + }) + + t.test('maintains transaction context over setInterval in-context', (t) => { + const { agent } = setupAgent(t, config) + + helper.runInTransaction(agent, function (txn) { + t.ok(txn, 'transaction should not be null') + + const segment = txn.trace.root + agent.tracer.bindFunction(function one() { + return new Promise(executor).then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + t.end() + }) + }, segment)() + + const wrapperTwo = agent.tracer.bindFunction(function () { + return two() + }, segment) + const wrapperThree = agent.tracer.bindFunction(function () { + return three() + }, segment) + + function executor(resolve) { + const ref = setInterval(() => { + clearInterval(ref) + + next().then(() => { + const tx = agent.getTransaction() + t.equal(tx ? tx.id : null, txn.id) + resolve() + }) + }, 1) + } + + function next() { + return Promise.resolve(wrapperTwo()) + } + + function two() { + return nextTwo() + } + + function nextTwo() { + return Promise.resolve(wrapperThree()) + } + + function three() {} + }) + }) +} + +function checkCallMetrics(t, testMetrics) { + // Tap also creates promises, so these counts don't quite match the tests. + const TAP_COUNT = 1 + + t.equal(testMetrics.initCalled - TAP_COUNT, 2, 'two promises were created') + t.equal(testMetrics.beforeCalled, 1, 'before hook called for all async promises') + t.equal( + testMetrics.beforeCalled, + testMetrics.afterCalled, + 'before should be called as many times as after' + ) + + if (global.gc) { + global.gc() + return setTimeout(function () { + t.equal( + testMetrics.initCalled - TAP_COUNT, + testMetrics.destroyCalled, + 'all promises created were destroyed' + ) + t.end() + }, 10) + } + t.end() +} + +test('promise hooks', function (t) { + t.autoend() + const testMetrics = { + initCalled: 0, + beforeCalled: 0, + afterCalled: 0, + destroyCalled: 0 + } + + const promiseIds = {} + const hook = asyncHooks.createHook({ + init: function initHook(id, type) { + if (type === 'PROMISE') { + promiseIds[id] = true + testMetrics.initCalled++ + } + }, + before: function beforeHook(id) { + if (promiseIds[id]) { + testMetrics.beforeCalled++ + } + }, + after: function afterHook(id) { + if (promiseIds[id]) { + testMetrics.afterCalled++ + } + }, + destroy: function destHook(id) { + if (promiseIds[id]) { + testMetrics.destroyCalled++ + } + } + }) + hook.enable() + + t.test('are only called once during the lifetime of a promise', function (t) { + new Promise(function (res) { + setTimeout(res, 10) + }).then(function () { + setImmediate(checkCallMetrics, t, testMetrics) + }) + }) +}) + +function setupAgent(t, config) { + const agent = helper.instrumentMockedAgent(config) + t.teardown(function () { + helper.unloadAgent(agent) + }) + + const contextManager = helper.getContextManager() + + return { + agent, + contextManager + } +} + +class TestResource extends asyncHooks.AsyncResource { + constructor(id) { + super('PROMISE', id) + } + + doStuff(callback) { + process.nextTick(() => { + if (this.runInAsyncScope) { + this.runInAsyncScope(callback) + } else { + this.emitBefore() + callback() + this.emitAfter() + } + }) + } +} diff --git a/test/integration/core/promises.js b/test/integration/core/promises.js deleted file mode 100644 index c5f145ef78..0000000000 --- a/test/integration/core/promises.js +++ /dev/null @@ -1,682 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const genericTestDir = '../../integration/instrumentation/promises/' -const helper = require('../../lib/agent_helper') -const util = require('util') -const testPromiseSegments = require(genericTestDir + 'segments') -const testTransactionState = require(genericTestDir + 'transaction-state') - -module.exports = function runTests(t, flags) { - t.test('transaction state', function (t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - t.autoend() - testTransactionState(t, agent, Promise) - }) - - // XXX Promise segments in native instrumentation are currently less than ideal - // XXX in structure. Transaction state is correctly maintained, and all segments - // XXX are created, but the heirarchy is not correct. - t.test('segments', function (t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - t.autoend() - testPromiseSegments(t, agent, Promise) - }) - - t.test('then', function testThen(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(executor).then(done, fail) - - function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - accept(15) - reject(10) - }, 0) - } - - function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 15, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('multi then', function testThen(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - accept(15) - reject(10) - }, 0) - }) - .then(next, fail) - .then(function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 15, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }, fail) - - function next(val) { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 15, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - return val - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('multi then async', function testThen(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - accept(15) - reject(10) - }, 0) - }) - .then(next, fail) - .then(function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 15, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }, fail) - - function next(val) { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 15, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - return new Promise(function wait(accept) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - accept(val) - }, 0) - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('then reject', function testThenReject(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(executor).then(fail, done) - - function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(10) - accept(15) - }, 0) - } - - function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('multi then reject', function testThen(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(10) - accept(15) - }, 0) - }) - .then(fail, next) - .then(fail, done) - - function next(val) { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - throw val - } - - function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('multi then async reject', function testThen(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(10) - accept(15) - }, 0) - }) - .then(fail, next) - .then(fail, function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }) - - function next(val) { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - return new Promise(function wait(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(val) - }, 0) - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('catch', function testCatch(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(10) - accept(15) - }, 0) - }).catch(function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }) - }) - }) - - t.test('multi catch', function testThen(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(10) - accept(15) - }, 0) - }) - .catch(function next(val) { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - throw val - }) - .catch(function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }) - }) - }) - - t.test('multi catch async', function testThen(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function executor(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(10) - accept(15) - }, 0) - }) - .catch(function next(val) { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - return new Promise(function wait(accept, reject) { - segment = agent.tracer.getSegment() - setTimeout(function resolve() { - reject(val) - }, 0) - }) - }) - .catch(function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 10, 'should resolve with the correct value') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }) - }) - }) - - t.test('Promise.resolve', function testResolve(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - setTimeout(function resolve() { - Promise.resolve(15) - .then(function (val) { - segment = agent.tracer.getSegment() - return val - }) - .then(done, fail) - }, 0) - - function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.equal(val, 15, 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('Promise.reject', function testReject(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - setTimeout(function reject() { - Promise.reject(10) - .then(null, function (error) { - segment = agent.tracer.getSegment() - throw error - }) - .then(fail, function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal( - id(agent.getTransaction()), - id(transaction), - 'transaction should be preserved' - ) - t.equal(val, 10, 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }) - }, 0) - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('Promise.all', function testAll(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - setTimeout(function resolve() { - const a = Promise.resolve(15) - const b = Promise.resolve(25) - Promise.all([a, b]) - .then(function (val) { - segment = agent.tracer.getSegment() - return val - }) - .then(done, fail) - }, 0) - - function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.same(val, [15, 25], 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('Promise.all reject', function testAllReject(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - setTimeout(function reject() { - const a = Promise.resolve(15) - const b = Promise.reject(10) - Promise.all([a, b]) - .then(null, function (err) { - segment = agent.tracer.getSegment() - throw err - }) - .then(fail, done) - }, 0) - - function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.same(val, 10, 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('Promise.race', function testRace(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - setTimeout(function () { - const a = Promise.resolve(15) - const b = new Promise(function (resolve) { - setTimeout(resolve, 100) - }) - Promise.race([a, b]) - .then(function (val) { - segment = agent.tracer.getSegment() - return val - }) - .then(done, fail) - }, 0) - - function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal(id(agent.getTransaction()), id(transaction), 'transaction should be preserved') - t.same(val, 15, 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('Promise.race reject', function testRaceReject(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment - - helper.runInTransaction(agent, function inTransaction(transaction) { - setTimeout(function reject() { - const a = new Promise(function (resolve) { - setTimeout(resolve, 100) - }) - const b = Promise.reject(10) - Promise.race([a, b]) - .then(null, function (err) { - segment = agent.tracer.getSegment() - throw err - }) - .then(fail, function done(val) { - t.equal(this, void 0, 'context should be undefined') - process.nextTick(function finish() { - t.equal( - id(agent.getTransaction()), - id(transaction), - 'transaction should be preserved' - ) - t.same(val, 10, 'value should be preserved') - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - }) - }, 0) - - function fail() { - t.fail('should not be called') - t.end() - } - }) - }) - - t.test('should throw when called without executor', function testNoExecutor(t) { - const OriginalPromise = Promise - let unwrappedError - let wrappedError - let wrapped - let unwrapped - helper.loadTestAgent(t, { feature_flag: flags }) - - try { - unwrapped = new OriginalPromise(null) - } catch (err) { - unwrappedError = err - } - - try { - wrapped = new Promise(null) - } catch (err) { - wrappedError = err - } - - t.equal(wrapped, void 0, 'should not be set') - t.equal(unwrapped, void 0, 'should not be set') - t.ok(unwrappedError instanceof Error, 'should error') - t.ok(wrappedError instanceof Error, 'should error') - t.equal(wrappedError.message, unwrappedError.message, 'should have same message') - - t.end() - }) - - t.test('should work if something wraps promises first', function testWrapSecond(t) { - const OriginalPromise = Promise - - util.inherits(WrappedPromise, Promise) - global.Promise = WrappedPromise - - function WrappedPromise(executor) { - const promise = new OriginalPromise(executor) - promise.__proto__ = WrappedPromise.prototype - return promise - } - - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - t.teardown(function () { - global.Promise = OriginalPromise - }) - - helper.runInTransaction(agent, function () { - const p = new Promise(function noop() {}) - - t.ok(p instanceof Promise, 'instanceof should work on nr wrapped Promise') - t.ok(p instanceof WrappedPromise, 'instanceof should work on wrapped Promise') - t.ok(p instanceof OriginalPromise, 'instanceof should work on unwrapped Promise') - - t.end() - }) - }) - - t.test('should work if something wraps promises after', function testWrapFirst(t) { - const OriginalPromise = Promise - - helper.loadTestAgent(t, { feature_flag: flags }) - util.inherits(WrappedPromise, Promise) - global.Promise = WrappedPromise - - t.teardown(function () { - global.Promise = OriginalPromise - }) - - /* eslint-disable-next-line sonarjs/no-identical-functions -- Disabled due to wrapping behavior and scoping issue */ - function WrappedPromise(executor) { - const promise = new OriginalPromise(executor) - promise.__proto__ = WrappedPromise.prototype - return promise - } - - const p = new Promise(function noop() {}) - - t.ok(p instanceof Promise, 'instanceof should work on nr wrapped Promise') - t.ok(p instanceof WrappedPromise, 'instanceof should work on wrapped Promise') - t.ok(p instanceof OriginalPromise, 'instanceof should work on unwrapped Promise') - - t.end() - }) - - t.test('throw in executor', function testCatch(t) { - const agent = helper.loadTestAgent(t, { feature_flag: flags }) - let segment = null - const exception = {} - - helper.runInTransaction(agent, function inTransaction(transaction) { - new Promise(function () { - segment = agent.tracer.getSegment() - throw exception - }).then( - function () { - t.fail('should have rejected promise') - t.end() - }, - function (val) { - t.equal(this, undefined, 'context should be undefined') - - process.nextTick(function () { - const keptTx = agent.tracer.getTransaction() - t.equal(keptTx && keptTx.id, transaction.id, 'transaction should be preserved') - t.equal(val, exception, 'should pass through error') - - // Using `.ok` intead of `.equal` to avoid giant test message that is - // not useful in this case. - t.ok(agent.tracer.getSegment() === segment, 'segment should be preserved') - - t.end() - }) - } - ) - }) - }) -} - -function id(tx) { - return tx && tx.id -} diff --git a/test/integration/core/promises.tap.js b/test/integration/core/promises.tap.js deleted file mode 100644 index 91992a4e7b..0000000000 --- a/test/integration/core/promises.tap.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const { test } = require('tap') - -const runTests = require('./promises') - -test('Promises (await_support: false)', (t) => { - t.autoend() - - runTests(t, { - await_support: false, - legacy_context_manager: true - }) -}) - -test('Promises (await_support: true)', (t) => { - t.autoend() - - runTests(t, { - await_support: true, - legacy_context_manager: true - }) -}) diff --git a/test/integration/core/timers.tap.js b/test/integration/core/timers.tap.js index 26859d7902..85c63e088c 100644 --- a/test/integration/core/timers.tap.js +++ b/test/integration/core/timers.tap.js @@ -93,15 +93,22 @@ tap.test('setImmediate', function testSetImmediate(t) { }) t.test('should not propagate segments for ended transaction', (t) => { - const { agent, contextManager } = setupAgent(t) + const { agent } = setupAgent(t) t.notOk(agent.getTransaction(), 'should not start in a transaction') helper.runInTransaction(agent, (transaction) => { transaction.end() - setImmediate(() => { - t.notOk(contextManager.getContext(), 'should not have segment for ended transaction') - t.end() + helper.runInSegment(agent, 'test-segment', () => { + const segment = agent.tracer.getSegment() + t.not(segment.name, 'test-segment') + t.equal(segment.children.length, 0, 'should not propagate segments when transaction ends') + setImmediate(() => { + const segment = agent.tracer.getSegment() + t.not(segment.name, 'test-segment') + t.equal(segment.children.length, 0, 'should not propagate segments when transaction ends') + t.end() + }) }) }) }) @@ -281,13 +288,7 @@ tap.test('clearTimeout should not ignore parent segment when internal', (t) => { }) function setupAgent(t) { - const config = { - feature_flag: { - legacy_context_manager: true - } - } - - const agent = helper.instrumentMockedAgent(config) + const agent = helper.instrumentMockedAgent() const contextManager = helper.getContextManager() t.teardown(function tearDown() { diff --git a/test/integration/instrumentation/promises/segments.js b/test/integration/instrumentation/promises/segments.js deleted file mode 100644 index d12b3d80ea..0000000000 --- a/test/integration/instrumentation/promises/segments.js +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const helper = require('../../../lib/agent_helper') -// load the assertSegments assertion -require('../../../lib/metrics_helper') - -module.exports = runTests - -function runTests(t, agent, Promise) { - segmentsEnabledTests(t, agent, Promise, doSomeWork) - segmentsDisabledTests(t, agent, Promise, doSomeWork) - - // simulates a function that returns a promise and has a segment created for itself - function doSomeWork(segmentName, shouldReject) { - const tracer = agent.tracer - const segment = tracer.createSegment(segmentName) - return tracer.bindFunction(actualWork, segment)() - function actualWork() { - segment.touch() - return new Promise(function startSomeWork(resolve, reject) { - if (shouldReject) { - process.nextTick(function () { - reject('some reason') - }) - } else { - process.nextTick(function () { - resolve(123) - }) - } - }) - } - } -} - -function segmentsEnabledTests(t, agent, Promise, doSomeWork) { - const tracer = agent.tracer - - t.test('segments: child segment is created inside then handler', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 2) - - t.assertSegments(tx.trace.root, ['doSomeWork', 'someChildSegment']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doSomeWork').then(function () { - const childSegment = tracer.createSegment('someChildSegment') - // touch the segment, so that it is not truncated - childSegment.touch() - tracer.bindFunction(function () {}, childSegment) - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: then handler that returns a new promise', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 3) - t.assertSegments(tx.trace.root, ['doWork1', 'doWork2', 'secondThen']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return doSomeWork('doWork2') - }) - .then(function secondThen() { - const s = tracer.createSegment('secondThen') - s.start() - s.end() - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: then handler that returns a value', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return 'some value' - }) - .then(function secondThen() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: catch handler with error from original promise', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1', true) - .then(function firstThen() { - return 'some value' - }) - .catch(function catchHandler() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: catch handler with error from subsequent promise', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 3) - t.assertSegments(tx.trace.root, ['doWork1', 'doWork2', 'catchHandler']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return doSomeWork('doWork2', true) - }) - .then(function secondThen() { - const s = tracer.createSegment('secondThen') - s.start() - s.end() - }) - .catch(function catchHandler() { - const s = tracer.createSegment('catchHandler') - s.start() - s.end() - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: when promise is created beforehand', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doSomeWork'], true) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - let resolve - const p = new Promise(function startSomeWork(r) { - resolve = r - }) - - const segment = tracer.createSegment('doSomeWork') - resolve = tracer.bindFunction(resolve, segment) - - p.then(function myThen() { - segment.touch() - process.nextTick(transaction.end.bind(transaction)) - }) - - // Simulate call that resolves the promise, but its segment is created - // after the promise is created - resolve() - }) - }) -} - -function segmentsDisabledTests(t, agent, Promise, doSomeWork) { - const tracer = agent.tracer - - t.test('no segments: child segment is created inside then handler', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 2) - - t.assertSegments(tx.trace.root, ['doSomeWork', 'someChildSegment']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doSomeWork').then(function () { - const childSegment = tracer.createSegment('someChildSegment') - // touch the segment, so that it is not truncated - childSegment.touch() - tracer.bindFunction(function () {}, childSegment) - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: then handler that returns a new promise', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return new Promise(function secondChain(res) { - res() - }) - }) - .then(function secondThen() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: then handler that returns a value', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return 'some value' - }) - .then(function secondThen() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: catch handler with error from original promise', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1', true) - .then(function firstThen() { - return 'some value' - }) - .catch(function catchHandler() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: catch handler with error from subsequent promise', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 2) - - t.assertSegments(tx.trace.root, ['doWork1', 'doWork2']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return doSomeWork('doWork2', true) - }) - .then(function secondThen() {}) - .catch(function catchHandler() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: when promise is created beforehand', function (t) { - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doSomeWork'], true) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - let resolve - const p = new Promise(function startSomeWork(r) { - resolve = r - }) - - const segment = tracer.createSegment('doSomeWork') - resolve = tracer.bindFunction(resolve, segment) - - p.then(function myThen() { - segment.touch() - process.nextTick(transaction.end.bind(transaction)) - }) - - // Simulate call that resolves the promise, but its segment is created - // after the promise is created. - resolve() - }) - }) -} diff --git a/test/lib/custom-assertions.js b/test/lib/custom-assertions.js index 9d96084111..78be0f138c 100644 --- a/test/lib/custom-assertions.js +++ b/test/lib/custom-assertions.js @@ -25,8 +25,9 @@ function assertExactClmAttrs(segmentStub, expectedAttrs) { * @param {object} params * @param {object} params.segments list of segments to assert { segment, filepath, name } * @param {boolean} params.enabled if CLM is enabled or not + * @param {boolean} params.skipFull flag to skip asserting `code.lineno` and `code.column` */ -function assertCLMAttrs({ segments, enabled: clmEnabled }) { +function assertCLMAttrs({ segments, enabled: clmEnabled, skipFull = false }) { segments.forEach((segment) => { const attrs = segment.segment.getAttributes() if (clmEnabled) { @@ -35,8 +36,11 @@ function assertCLMAttrs({ segments, enabled: clmEnabled }) { attrs['code.filepath'].endsWith(segment.filepath), 'should have appropriate code.filepath' ) - this.match(attrs['code.lineno'], /[\d]+/, 'lineno should be a number') - this.match(attrs['code.column'], /[\d]+/, 'column should be a number') + + if (!skipFull) { + this.match(attrs['code.lineno'], /[\d]+/, 'lineno should be a number') + this.match(attrs['code.column'], /[\d]+/, 'column should be a number') + } } else { this.notOk(attrs['code.function'], 'function should not exist') this.notOk(attrs['code.filepath'], 'filepath should not exist') diff --git a/test/lib/params.js b/test/lib/params.js index 60f82472e4..22dffd2d3d 100644 --- a/test/lib/params.js +++ b/test/lib/params.js @@ -15,11 +15,6 @@ module.exports = { mongodb_host: process.env.NR_NODE_TEST_MONGODB_HOST || 'localhost', mongodb_port: process.env.NR_NODE_TEST_MONGODB_PORT || 27017, - // mongodb 4.2.0 does not allow mongo server v2. - // There is now a separate container that maps 27018 to mongo:5 - mongodb_v4_host: process.env.NR_NODE_TEST_MONGODB_V4_HOST || 'localhost', - mongodb_v4_port: process.env.NR_NODE_TEST_MONGODB_V4_PORT || 27018, - mysql_host: process.env.NR_NODE_TEST_MYSQL_HOST || 'localhost', mysql_port: process.env.NR_NODE_TEST_MYSQL_PORT || 3306, diff --git a/test/integration/instrumentation/promises/transaction-state.js b/test/lib/promises/transaction-state.js similarity index 99% rename from test/integration/instrumentation/promises/transaction-state.js rename to test/lib/promises/transaction-state.js index fdf6164417..901a0cd48f 100644 --- a/test/integration/instrumentation/promises/transaction-state.js +++ b/test/lib/promises/transaction-state.js @@ -5,7 +5,7 @@ 'use strict' -const helper = require('../../../lib/agent_helper') +const helper = require('../agent_helper') const COUNT = 2 diff --git a/test/lib/test-reporter.mjs b/test/lib/test-reporter.mjs new file mode 100644 index 0000000000..6c47ee2b4d --- /dev/null +++ b/test/lib/test-reporter.mjs @@ -0,0 +1,65 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// This file provides a custom test reporter for the native test runner +// included in Node.js >=18. The default `spec` reporter writes too much +// information to be usable in CI, and the `dot` reporter hides which tests +// failed. This custom reporter outputs nothing for successful tests, and +// outputs the failing test file when any failing test has occurred. +// +// See https://nodejs.org/api/test.html#custom-reporters. + +const OUTPUT_MODE = process.env.OUTPUT_MODE?.toLowerCase() ?? 'simple' +const isSilent = OUTPUT_MODE === 'quiet' || OUTPUT_MODE === 'silent' + +async function* reporter(source) { + const passed = new Set() + const failed = new Set() + + for await (const event of source) { + // Once v18 has been dropped, we might want to revisit the output of + // cases. The `event` object is supposed to provide things like + // the failing line number and column, along with the failing test name. + // But on v18, we seem to only get `1` for both line and column, and the + // test name gets set to the `file`. So there isn't really any point in + // trying to provide more useful reports here while we need to support v18. + // + // The issue may also stem from the current test suites still being based + // on `tap`. Once we are able to migrate the actual test code to `node:test` + // we should revisit this reporter to determine if we can improve it. + // + // See https://nodejs.org/api/test.html#event-testfail. + switch (event.type) { + case 'test:pass': { + passed.add(event.data.file) + if (isSilent === true) { + yield '' + } else { + yield `passed: ${event.data.file}\n` + } + + break + } + + case 'test:fail': { + failed.add(event.data.file) + yield `failed: ${event.data.file}\n` + break + } + + default: { + yield '' + } + } + } + + yield `\n\nFailed tests:\n` + for (const file of failed) { + yield `${file}\n` + } + yield `\n\nPassed: ${passed.size}\nFailed: ${failed.size}\nTotal: ${passed.size + failed.size}\n` +} + +export default reporter diff --git a/test/unit/context-manager/context-manager-tests.js b/test/unit/context-manager/context-manager-tests.js index faff610eb2..58098277a6 100644 --- a/test/unit/context-manager/context-manager-tests.js +++ b/test/unit/context-manager/context-manager-tests.js @@ -9,7 +9,7 @@ * Add a standard set of Legacy Context Manager test cases for testing * either the standard or diagnostic versions. */ -function runLegacyTests(t, createContextManager) { +function runContextMangerTests(t, createContextManager) { t.test('Should default to null context', (t) => { const contextManager = createContextManager() @@ -170,4 +170,4 @@ function runLegacyTests(t, createContextManager) { }) } -module.exports = runLegacyTests +module.exports = runContextMangerTests diff --git a/test/unit/context-manager/create-context-manager.test.js b/test/unit/context-manager/create-context-manager.test.js index 2d548cbb67..88998e1a04 100644 --- a/test/unit/context-manager/create-context-manager.test.js +++ b/test/unit/context-manager/create-context-manager.test.js @@ -8,7 +8,6 @@ const { test } = require('tap') const createImplementation = require('../../../lib/context-manager/create-context-manager') -const LegacyContextManager = require('../../../lib/context-manager/legacy-context-manager') const AsyncLocalContextManager = require('../../../lib/context-manager/async-local-context-manager') test('Should return AsyncLocalContextManager by default', (t) => { @@ -20,13 +19,3 @@ test('Should return AsyncLocalContextManager by default', (t) => { t.ok(contextManager instanceof AsyncLocalContextManager) t.end() }) - -test('Should return LegacyContextManager when enabled', (t) => { - const contextManager = createImplementation({ - logging: {}, - feature_flag: { legacy_context_manager: true } - }) - - t.ok(contextManager instanceof LegacyContextManager) - t.end() -}) diff --git a/test/unit/context-manager/legacy-context-manager.test.js b/test/unit/context-manager/legacy-context-manager.test.js deleted file mode 100644 index 9a31dbea19..0000000000 --- a/test/unit/context-manager/legacy-context-manager.test.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const { test } = require('tap') - -const runContextManagerTests = require('./context-manager-tests') -const LegacyContextManager = require('../../../lib/context-manager/legacy-context-manager') - -test('Legacy Context Manager', (t) => { - t.autoend() - - runContextManagerTests(t, createLegacyContextManager) -}) - -function createLegacyContextManager() { - return new LegacyContextManager({}) -} diff --git a/test/unit/index.test.js b/test/unit/index.test.js index babc2b7469..fc0a0f24f8 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -132,28 +132,6 @@ test('loader metrics', (t) => { t.end() }) - t.test('should load preload unsupported metric if node version is <16.2.0', (t) => { - const processVersionStub = { - satisfies: sandbox.stub() - } - processVersionStub.satisfies.onCall(0).returns(false) - processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(true) - process.execArgv = ['--loader', 'newrelic/esm-loader.mjs'] - const agent = proxyquire('../../index', { - './lib/util/process-version': processVersionStub, - './lib/agent': MockAgent, - './lib/shimmer': shimmerMock, - './api': ApiMock - }) - - const metricCall = agent.agent.metrics.getOrCreateMetric - - t.equal(metricCall.args.length, 2) - t.equal(metricCall.args[0][0], 'Supportability/Features/ESM/UnsupportedLoader') - t.end() - }) - t.test('should load require metric when agent is required', (t) => { const agent = proxyquire('../../index', { './lib/agent': MockAgent, @@ -222,8 +200,7 @@ test('index tests', (t) => { sandbox.stub(console, 'error') k2Stub = { start: sandbox.stub() } processVersionStub.satisfies.onCall(0).returns(true) - processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(false) + processVersionStub.satisfies.onCall(1).returns(false) mockConfig.applications.returns(['my-app-name']) MockAgent.prototype.start.yields(null) shimmerMock = createShimmerMock(sandbox) @@ -287,7 +264,7 @@ test('index tests', (t) => { }) t.test('should throw error if using an unsupported version of Node.js', (t) => { - processVersionStub.satisfies.onCall(1).returns(false) + processVersionStub.satisfies.onCall(0).returns(false) loadIndex() t.equal(loggerMock.error.callCount, 1, 'should log an error') t.match(loggerMock.error.args[0][0], /New Relic for Node.js requires a version of Node/) @@ -297,7 +274,6 @@ test('index tests', (t) => { t.test('should log warning if using an odd version of node', (t) => { processVersionStub.satisfies.onCall(0).returns(true) processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(true) configMock.getOrCreateInstance.returns(null) loadIndex() t.equal(loggerMock.warn.callCount, 1, 'should log an error') @@ -308,8 +284,7 @@ test('index tests', (t) => { t.test('should use stub api if no config detected', (t) => { configMock.getOrCreateInstance.returns(null) processVersionStub.satisfies.onCall(0).returns(true) - processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(false) + processVersionStub.satisfies.onCall(1).returns(false) const api = loadIndex() t.equal(loggerMock.info.callCount, 2, 'should log info logs') t.equal(loggerMock.info.args[1][0], 'No configuration detected. Not starting.') @@ -320,8 +295,7 @@ test('index tests', (t) => { t.test('should use stub api if agent_enabled is false', (t) => { configMock.getOrCreateInstance.returns({ agent_enabled: false }) processVersionStub.satisfies.onCall(0).returns(true) - processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(false) + processVersionStub.satisfies.onCall(1).returns(false) const api = loadIndex() t.equal(loggerMock.info.callCount, 2, 'should log info logs') t.equal(loggerMock.info.args[1][0], 'Module disabled in configuration. Not starting.') @@ -332,8 +306,7 @@ test('index tests', (t) => { t.test('should log warning when logging diagnostics is enabled', (t) => { mockConfig.logging.diagnostics = true processVersionStub.satisfies.onCall(0).returns(true) - processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(false) + processVersionStub.satisfies.onCall(1).returns(false) loadIndex() t.equal( loggerMock.warn.args[0][0], @@ -344,8 +317,7 @@ test('index tests', (t) => { t.test('should throw error is app name is not set in config', (t) => { processVersionStub.satisfies.onCall(0).returns(true) - processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(false) + processVersionStub.satisfies.onCall(1).returns(false) mockConfig.applications.returns([]) loadIndex() t.equal(loggerMock.error.callCount, 1, 'should log an error') @@ -355,8 +327,7 @@ test('index tests', (t) => { t.test('should log error if agent startup failed', (t) => { processVersionStub.satisfies.onCall(0).returns(true) - processVersionStub.satisfies.onCall(1).returns(true) - processVersionStub.satisfies.onCall(2).returns(false) + processVersionStub.satisfies.onCall(1).returns(false) mockConfig.applications.returns(['my-app-name']) const err = new Error('agent start failed') MockAgent.prototype.start.yields(err) diff --git a/test/unit/instrumentation/core/promises.test.js b/test/unit/instrumentation/core/promises.test.js index 721363dd3b..4e81501a2f 100644 --- a/test/unit/instrumentation/core/promises.test.js +++ b/test/unit/instrumentation/core/promises.test.js @@ -12,7 +12,7 @@ const helper = require('../../../lib/agent_helper') /** * Note: These test had more meaning when we had legacy promise tracking. - * We now rely on async hooks to do to promise async propagation. But unlike legacy + * We now rely on AsyncLocalStorage context maanger to do to promise async propagation. But unlike legacy * promise instrumentation this will only propagate the same base promise segment. * * The tests still exist to prove some more complex promise chains will not lose context @@ -23,13 +23,7 @@ test('Promise trace', (t) => { let agent = null t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ - feature_flag: { - promise_segments: true, - await_support: false, - legacy_context_manager: true - } - }) + agent = helper.instrumentMockedAgent() }) t.afterEach(() => { diff --git a/test/unit/instrumentation/mongodb.test.js b/test/unit/instrumentation/mongodb.test.js new file mode 100644 index 0000000000..66cfc35b83 --- /dev/null +++ b/test/unit/instrumentation/mongodb.test.js @@ -0,0 +1,54 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') + +const helper = require('../../lib/agent_helper') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +tap.beforeEach((t) => { + const sandbox = sinon.createSandbox() + t.context.sandbox = sandbox + t.context.agent = helper.loadMockedAgent() + t.context.initialize = proxyquire('../../../lib/instrumentation/mongodb', { + './mongodb/v4-mongo': function stub() {} + }) + const shim = { + setDatastore: sandbox.stub(), + pkgVersion: '4.0.0', + logger: { + warn: sandbox.stub() + } + } + shim.pkgVersion = '4.0.0' + t.context.shim = shim +}) + +tap.afterEach((t) => { + helper.unloadAgent(t.context.agent) + t.context.sandbox.restore() +}) + +tap.test('should not log warning if version is >= 4', function (t) { + const { agent, shim, initialize } = t.context + initialize(agent, {}, 'mongodb', shim) + t.equal(shim.logger.warn.callCount, 0) + t.equal(shim.setDatastore.callCount, 1) + t.end() +}) + +tap.test('should log warning if using unsupported version of mongo', function (t) { + const { agent, shim, initialize } = t.context + shim.pkgVersion = '2.0.0' + initialize(agent, {}, 'mongodb', shim) + t.same(shim.logger.warn.args[0], [ + 'New Relic Node.js agent no longer supports mongodb < 4, current version %s. Please downgrade to v11 for support, if needed', + '2.0.0' + ]) + t.end() +}) diff --git a/test/unit/instrumentation/nextjs/next-server.test.js b/test/unit/instrumentation/nextjs/next-server.test.js new file mode 100644 index 0000000000..0b7e6707a9 --- /dev/null +++ b/test/unit/instrumentation/nextjs/next-server.test.js @@ -0,0 +1,77 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const sinon = require('sinon') +const initialize = require('../../../../lib/instrumentation/nextjs/next-server') +const helper = require('../../../lib/agent_helper') + +tap.test('middleware tracking', (t) => { + t.autoend() + let MockServer + let agent + let shim + + function createMockServer() { + function FakeServer() {} + FakeServer.prototype.renderToResponseWithComponents = sinon.stub() + FakeServer.prototype.runApi = sinon.stub() + FakeServer.prototype.renderHTML = sinon.stub() + FakeServer.prototype.runMiddleware = sinon.stub() + return FakeServer + } + + t.beforeEach(() => { + agent = helper.loadMockedAgent() + const Shim = require(`../../../../lib/shim/webframework-shim`) + shim = new Shim(agent, './next-server') + sinon.stub(shim, 'require') + sinon.stub(shim, 'setFramework') + shim.require.returns({ version: '12.2.0' }) + sinon.spy(shim.logger, 'warn') + + MockServer = createMockServer() + initialize(shim, { default: MockServer }) + }) + + t.afterEach(() => { + helper.unloadAgent(agent) + }) + + t.test( + 'should instrument renderHTML, runMiddleware, runApi, and renderToResponseWithComponents', + (t) => { + t.ok(shim.isWrapped(MockServer.prototype.runMiddleware)) + t.ok(shim.isWrapped(MockServer.prototype.runApi)) + t.ok(shim.isWrapped(MockServer.prototype.renderHTML)) + t.ok(shim.isWrapped(MockServer.prototype.renderToResponseWithComponents)) + t.equal( + shim.logger.warn.callCount, + 0, + 'should not long warning on middleware not being instrumented' + ) + t.end() + } + ) + + t.test('should not instrument runMiddleware if Next.js < 12.2.0', (t) => { + shim.require.returns({ version: '12.0.1' }) + const NewFakeServer = createMockServer() + initialize(shim, { default: NewFakeServer }) + t.equal(shim.logger.warn.callCount, 1, 'should log warn message') + const loggerArgs = shim.logger.warn.args[0] + t.same(loggerArgs, [ + 'Next.js middleware instrumentation only supported on >=12.2.0 <=13.4.12, got %s', + '12.0.1' + ]) + t.notOk( + shim.isWrapped(NewFakeServer.prototype.runMiddleware), + 'should not wrap getModuleContext when version is less than 12.2.0' + ) + t.end() + }) +}) diff --git a/test/unit/instrumentation/nextjs/utils.test.js b/test/unit/instrumentation/nextjs/utils.test.js new file mode 100644 index 0000000000..fd9fdcfd2e --- /dev/null +++ b/test/unit/instrumentation/nextjs/utils.test.js @@ -0,0 +1,49 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const sinon = require('sinon') +const { assignCLMAttrs } = require('../../../../lib/instrumentation/nextjs/utils') + +tap.test('assignCLMAttrs', (t) => { + t.autoend() + const config = { code_level_metrics: { enabled: true } } + let segmentStub + + t.beforeEach(() => { + segmentStub = { + addAttribute: sinon.stub() + } + }) + + t.test('should add attrs to segment', (t) => { + const attrs = { + 'code.function': 'foo', + 'code.filepath': 'pages/foo/bar' + } + assignCLMAttrs(config, segmentStub, attrs) + t.equal(segmentStub.addAttribute.callCount, 2) + t.same(segmentStub.addAttribute.args, [ + ['code.function', 'foo'], + ['code.filepath', 'pages/foo/bar'] + ]) + t.end() + }) + + t.test('should not add attr is code_level_metrics is disabled', (t) => { + config.code_level_metrics = null + assignCLMAttrs(config, segmentStub) + t.notOk(segmentStub.addAttribute.callCount) + t.end() + }) + + t.test('should not add attribute if segment is undefined', (t) => { + assignCLMAttrs(config, null) + t.notOk(segmentStub.addAttribute.callCount) + t.end() + }) +}) diff --git a/test/unit/load-externals.test.js b/test/unit/load-externals.test.js new file mode 100644 index 0000000000..bec9c45a8d --- /dev/null +++ b/test/unit/load-externals.test.js @@ -0,0 +1,27 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const loadExternals = require('../../load-externals') + +tap.test('should load libs to webpack externals', async (t) => { + const config = { + target: 'node-20.x', + externals: ['next'] + } + loadExternals(config) + t.ok(config.externals.length > 1, 'should add all libraries agent supports to the externals list') +}) + +tap.test('should not add externals when target is not node', async (t) => { + const config = { + target: 'web', + externals: ['next'] + } + loadExternals(config) + t.ok(config.externals.length === 1, 'should not agent libraries when target is not node') +}) diff --git a/test/versioned-external/external-repos.js b/test/versioned-external/external-repos.js index 73b4b52412..396200a2f9 100644 --- a/test/versioned-external/external-repos.js +++ b/test/versioned-external/external-repos.js @@ -12,11 +12,6 @@ * additionalFiles: String array of files/folders to checkout in addition to lib and tests/versioned. */ const repos = [ - { - name: 'next', - repository: 'https://github.com/newrelic/newrelic-node-nextjs.git', - branch: 'main' - }, { name: 'apollo-server', repository: 'https://github.com/newrelic/newrelic-node-apollo-server-plugin.git', diff --git a/test/versioned/amqplib/package.json b/test/versioned/amqplib/package.json index bce99f8324..a793829d19 100644 --- a/test/versioned/amqplib/package.json +++ b/test/versioned/amqplib/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "amqplib": ">=0.5.0" diff --git a/test/versioned/aws-sdk-v2/package.json b/test/versioned/aws-sdk-v2/package.json index 55b54fa3cb..f1cac3e1ab 100644 --- a/test/versioned/aws-sdk-v2/package.json +++ b/test/versioned/aws-sdk-v2/package.json @@ -8,7 +8,7 @@ "tests": [ { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "aws-sdk": { @@ -21,7 +21,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "aws-sdk": { @@ -41,7 +41,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "aws-sdk": { diff --git a/test/versioned/aws-sdk-v3/package.json b/test/versioned/aws-sdk-v3/package.json index 67ccb68a7f..75e9a76be0 100644 --- a/test/versioned/aws-sdk-v3/package.json +++ b/test/versioned/aws-sdk-v3/package.json @@ -1,7 +1,7 @@ { "name": "aws-sdk-v3-tests", "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "targets": [ {"name": "@aws-sdk/client-sqs", "minAgentVersion": "8.7.1"}, @@ -17,7 +17,7 @@ "tests": [ { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-api-gateway": { @@ -31,7 +31,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-elasticache": { @@ -45,7 +45,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-elastic-load-balancing": { @@ -59,7 +59,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-lambda": { @@ -73,7 +73,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-rds": { @@ -87,7 +87,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-redshift": { @@ -101,7 +101,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-rekognition": { @@ -115,7 +115,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-s3": { @@ -129,7 +129,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-ses": { @@ -143,7 +143,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-sns": { @@ -161,7 +161,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-sqs": { @@ -175,7 +175,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-dynamodb": { @@ -189,7 +189,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/util-dynamodb": "latest", @@ -205,7 +205,7 @@ }, { "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "dependencies": { "@aws-sdk/client-bedrock-runtime": "^3.474.0", diff --git a/test/versioned/bluebird/methods.js b/test/versioned/bluebird/methods.js index fd41e7bd8a..25aead2343 100644 --- a/test/versioned/bluebird/methods.js +++ b/test/versioned/bluebird/methods.js @@ -6,7 +6,7 @@ 'use strict' const helper = require('../../lib/agent_helper') -const testTransactionState = require('../../integration/instrumentation/promises/transaction-state') +const testTransactionState = require('../../lib/promises/transaction-state') const util = require('util') const symbols = require('../../../lib/symbols') diff --git a/test/versioned/bluebird/package.json b/test/versioned/bluebird/package.json index 9ee0ac4841..7eaf36bd2e 100644 --- a/test/versioned/bluebird/package.json +++ b/test/versioned/bluebird/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "bluebird": ">=2.0.0" @@ -18,7 +18,7 @@ }, { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "bluebird": ">=3.0.0" diff --git a/test/versioned/bluebird/transaction-state.tap.js b/test/versioned/bluebird/transaction-state.tap.js index 8db73d4355..1e2c10d337 100644 --- a/test/versioned/bluebird/transaction-state.tap.js +++ b/test/versioned/bluebird/transaction-state.tap.js @@ -5,11 +5,9 @@ 'use strict' -const testsDir = '../../integration/instrumentation/promises' - const helper = require('../../lib/agent_helper') const tap = require('tap') -const testTransactionState = require(testsDir + '/transaction-state') +const testTransactionState = require('../../lib/promises/transaction-state') tap.test('bluebird', function (t) { t.autoend() diff --git a/test/versioned/bunyan/package.json b/test/versioned/bunyan/package.json index 92c9a931f5..971c05278f 100644 --- a/test/versioned/bunyan/package.json +++ b/test/versioned/bunyan/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "bunyan": ">=1.8.12" diff --git a/test/versioned/cassandra-driver/package.json b/test/versioned/cassandra-driver/package.json index 09fd06d2ac..f2a010858a 100644 --- a/test/versioned/cassandra-driver/package.json +++ b/test/versioned/cassandra-driver/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "cassandra-driver": ">=3.4.0" diff --git a/test/versioned/cjs-in-esm/package.json b/test/versioned/cjs-in-esm/package.json index db6a59d9ef..9f30181dc0 100644 --- a/test/versioned/cjs-in-esm/package.json +++ b/test/versioned/cjs-in-esm/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { "express": "4.18.2", diff --git a/test/versioned/cls/package.json b/test/versioned/cls/package.json index c31041b5cf..bd87b64d6f 100644 --- a/test/versioned/cls/package.json +++ b/test/versioned/cls/package.json @@ -5,7 +5,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "continuation-local-storage": ">=3.0.0", diff --git a/test/versioned/connect/package.json b/test/versioned/connect/package.json index 267c6b92a8..da7490ca47 100644 --- a/test/versioned/connect/package.json +++ b/test/versioned/connect/package.json @@ -6,10 +6,10 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { - "connect": ">=2.0.0" + "connect": ">=3.0.0" }, "files": [ "error-intercept.tap.js", diff --git a/test/versioned/director/director.tap.js b/test/versioned/director/director.tap.js deleted file mode 100644 index eb643a6c89..0000000000 --- a/test/versioned/director/director.tap.js +++ /dev/null @@ -1,471 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const http = require('http') -const helper = require('../../lib/agent_helper') - -tap.test('basic director test', function (t) { - let server = null - const agent = helper.instrumentMockedAgent() - - const director = require('director') - - function fn0() { - t.ok(agent.getTransaction(), 'transaction is available') - this.res.writeHead(200, { 'Content-Type': 'application/json' }) - this.res.end('{"status":"ok"}') - } - - // this will still get hit even though the fn0 is ending response - function fn1() { - return true - } - - const routes = { - '/hello': { - 'get': fn0, - '/(\\w+)/': { - get: fn1 - } - } - } - - const router = new director.http.Router(routes).configure({ recurse: 'forward' }) - - t.teardown(function () { - helper.unloadAgent(agent) - server.close(function () {}) - }) - - // need to capture parameters - agent.config.attributes.enabled = true - - agent.on('transactionFinished', function (transaction) { - t.equal(transaction.name, 'WebTransaction/Director/GET//hello', 'transaction has expected name') - - t.equal(transaction.url, '/hello/eric', 'URL is left alone') - t.equal(transaction.statusCode, 200, 'status code is OK') - t.equal(transaction.verb, 'GET', 'HTTP method is GET') - t.ok(transaction.trace, 'transaction has trace') - - const web = transaction.trace.root.children[0] - t.ok(web, 'trace has web segment') - t.equal(web.name, transaction.name, 'segment name and transaction name match') - - t.equal(web.partialName, 'Director/GET//hello', 'should have partial name for apdex') - - const handler0 = web.children[0] - t.equal( - handler0.name, - 'Nodejs/Middleware/Director/fn0//hello', - 'route 0 segment has correct name' - ) - - const handler1 = web.children[1] - t.equal( - handler1.name, - 'Nodejs/Middleware/Director/fn1//hello/(\\w+)/', - 'route 1 segment has correct name' - ) - }) - - server = http.createServer(function (req, res) { - router.dispatch(req, res, function (err) { - if (err) { - res.writeHead(404) - res.end() - } - }) - }) - - helper.randomPort(function (port) { - server.listen(port, function () { - const url = 'http://localhost:' + port + '/hello/eric' - helper.makeGetRequest(url, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) -}) - -tap.test('backward recurse director test', function (t) { - let server = null - const agent = helper.instrumentMockedAgent() - - const director = require('director') - - function fn0() { - this.res.writeHead(200, { 'Content-Type': 'application/json' }) - this.res.end('{"status":"ok"}') - } - function fn1() { - null - } - - const routes = { - '/hello': { - 'get': fn0, - '/(\\w+)/': { - get: fn1 - } - } - } - - const router = new director.http.Router(routes).configure({ recurse: 'backward' }) - - t.teardown(function () { - helper.unloadAgent(agent) - server.close(function () {}) - }) - // need to capture parameters - agent.config.attributes.enabled = true - - agent.on('transactionFinished', function (transaction) { - t.equal(transaction.name, 'WebTransaction/Director/GET//hello', 'transaction has expected name') - - const web = transaction.trace.root.children[0] - t.equal(web.partialName, 'Director/GET//hello', 'should have partial name for apdex') - }) - - server = http.createServer(function (req, res) { - router.dispatch(req, res, function (err) { - if (err) { - res.writeHead(404) - res.end() - } - }) - }) - - helper.randomPort(function (port) { - server.listen(port, function () { - const url = 'http://localhost:' + port + '/hello/eric' - helper.makeGetRequest(url, { json: true }, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) -}) - -tap.test('two routers with same URI director test', function (t) { - let server = null - const agent = helper.instrumentMockedAgent() - - const director = require('director') - - const router = new director.http.Router() - - t.teardown(function () { - helper.unloadAgent(agent) - server.close(function () {}) - }) - - // need to capture parameters - agent.config.attributes.enabled = true - - agent.on('transactionFinished', function (transaction) { - t.equal( - transaction.name, - 'WebTransaction/Director/GET//helloWorld', - 'transaction has expected name' - ) - - const web = transaction.trace.root.children[0] - t.equal(web.partialName, 'Director/GET//helloWorld', 'should have partial name for apdex') - }) - - router.get('/helloWorld', function () {}) - router.get('/helloWorld', function () { - this.res.writeHead(200, { 'Content-Type': 'application/json' }) - this.res.end('{"status":"ok"}') - }) - - server = http.createServer(function (req, res) { - router.dispatch(req, res, function (err) { - if (err) { - res.writeHead(404) - res.end() - } - }) - }) - - helper.randomPort(function (port) { - server.listen(port, function () { - const url = 'http://localhost:' + port + '/helloWorld' - helper.makeGetRequest(url, { json: true }, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected response') - t.end() - }) - }) - }) -}) - -tap.test('director async routes test', function (t) { - let server = null - const agent = helper.instrumentMockedAgent() - - const director = require('director') - - const router = new director.http.Router().configure({ async: true }) - - t.teardown(function () { - helper.unloadAgent(agent) - server.close(function () {}) - }) - - // need to capture parameters - agent.config.attributes.enabled = true - - agent.on('transactionFinished', function (transaction) { - t.equal( - transaction.name, - 'WebTransaction/Director/GET//:foo/:bar/:bazz', - 'transaction has expected name' - ) - - const web = transaction.trace.root.children[0] - t.equal(web.partialName, 'Director/GET//:foo/:bar/:bazz', 'should have partial name for apdex') - - const handler0 = web.children[0] - - t.equal( - handler0.name, - 'Nodejs/Middleware/Director/fn0//:foo/:bar/:bazz', - 'route 0 segment has correct name' - ) - - const handler1 = web.children[1] - - t.equal( - handler1.name, - 'Nodejs/Middleware/Director/fn1//:foo/:bar/:bazz', - 'route 1 segment has correct name' - ) - }) - - router.get('/:foo/:bar/:bazz', function fn0(foo, bar, bazz, next) { - setTimeout( - function () { - next() - }, - 100, - this - ) - }) - router.get('/:foo/:bar/:bazz', function fn1() { - setTimeout( - function (self) { - self.res.end('dog') - }, - 100, - this - ) - }) - - server = http.createServer(function (req, res) { - router.dispatch(req, res, function (err) { - if (err) { - res.writeHead(404) - res.end() - } - }) - }) - - helper.randomPort(function (port) { - server.listen(port, function () { - const url = 'http://localhost:' + port + '/three/random/things' - helper.makeGetRequest(url, { json: true }, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, 'dog', 'got expected response') - t.end() - }) - }) - }) -}) - -tap.test('express w/ director subrouter test', function (t) { - t.plan(4) - const agent = helper.instrumentMockedAgent() - - const director = require('director') - - const express = require('express') - const expressRouter = express.Router() // eslint-disable-line new-cap - const app = express() - let server - - function helloWorld() { - this.res.writeHead(200, { 'Content-Type': 'text/plain' }) - this.res.end('eric says hello') - } - - const routes = { - '/hello': { get: helloWorld } - } - const router = new director.http.Router(routes) - - t.teardown(function () { - helper.unloadAgent(agent) - server.close(function () {}) - }) - - // need to capture parameters - agent.config.attributes.enabled = true - - agent.on('transactionFinished', function (transaction) { - t.equal( - transaction.name, - 'WebTransaction/Director/GET//express/hello', - 'transaction has expected name' - ) - - const web = transaction.trace.root.children[0] - t.equal(web.partialName, 'Director/GET//express/hello', 'should have partial name for apdex') - }) - - expressRouter.use(function myMiddleware(req, res, next) { - router.dispatch(req, res, function (err) { - if (err) { - next(err) - } - }) - }) - - app.use('/express/', expressRouter) - - helper.randomPort(function (port) { - server = app.listen(port, 'localhost', function () { - const url = 'http://localhost:' + port + '/express/hello' - helper.makeGetRequest(url, { json: true }, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, 'eric says hello', 'got expected response') - }) - }) - }) -}) - -tap.test('director instrumentation', function (t) { - t.plan(10) - - t.test('should allow null routers through constructor on http router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const routes = { - '/hello': null - } - - new director.http.Router(routes) // eslint-disable-line no-new - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow null routers through constructor on base router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const routes = { - '/hello': null - } - - new director.Router(routes) // eslint-disable-line no-new - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow null routers through constructor on cli router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const routes = { - '/hello': null - } - - new director.cli.Router(routes) // eslint-disable-line no-new - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow routers through .on on cli router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const router = new director.cli.Router() - router.on(/^$/, function () {}) - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow routers through .on on http router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const router = new director.http.Router() - router.on('get', /^$/, function () {}) - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow routers through .on on base router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const router = new director.Router() - router.on(/^$/, function () {}) - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow null routers through method mounters', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const router = new director.http.Router() - - router.get('/tes/', null) - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow null routers through .on on http router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const router = new director.http.Router() - - router.on('get', '/test/') - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow null routers through .on on cli router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const router = new director.cli.Router() - - router.on('get', 'test') - - helper.unloadAgent(agent) - t.end() - }) - - t.test('should allow null routers through .on on base router', function (t) { - const agent = helper.instrumentMockedAgent() - const director = require('director') - const router = new director.Router() - - router.on('get', 'test') - - helper.unloadAgent(agent) - t.end() - }) -}) diff --git a/test/versioned/director/newrelic.js b/test/versioned/director/newrelic.js deleted file mode 100644 index 5bfe53711f..0000000000 --- a/test/versioned/director/newrelic.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -exports.config = { - app_name: ['My Application'], - license_key: 'license key here', - logging: { - level: 'trace', - filepath: '../../../newrelic_agent.log' - }, - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - transaction_tracer: { - enabled: true - } -} diff --git a/test/versioned/director/package.json b/test/versioned/director/package.json deleted file mode 100644 index 3cfeb51f9f..0000000000 --- a/test/versioned/director/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "director-tests", - "targets": [{"name":"director","minAgentVersion":"2.0.0"}], - "version": "0.0.0", - "private": true, - "tests": [ - { - "engines": { - "node": ">=16" - }, - "dependencies": { - "director": ">=1.2.0", - "express": "4.16" - }, - "files": [ - "director.tap.js" - ] - } - ], - "dependencies": {} -} diff --git a/test/versioned/elastic/package.json b/test/versioned/elastic/package.json index 611bba8b42..876c188bc8 100644 --- a/test/versioned/elastic/package.json +++ b/test/versioned/elastic/package.json @@ -4,14 +4,14 @@ "version": "0.0.0", "private": true, "engines": { - "node": ">=16" + "node": ">=18" }, "tests": [ { "supported": false, "comment": "Used to assert our instrumentation does not get loaded on old versions.", "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "@elastic/elasticsearch": "7.13.0" @@ -20,18 +20,6 @@ "elasticsearchNoop.tap.js" ] }, - { - "engines": { - "node": "16" - }, - "dependencies": { - "@elastic/elasticsearch": ">=7.16.0 <=8.13.1", - "@elastic/transport": "8.4.1" - }, - "files": [ - "elasticsearch.tap.js" - ] - }, { "engines": { "node": ">=18" diff --git a/test/versioned/esm-package/package.json b/test/versioned/esm-package/package.json index 81f68e1f85..b95f430158 100644 --- a/test/versioned/esm-package/package.json +++ b/test/versioned/esm-package/package.json @@ -7,7 +7,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { "parse-json": "6.0.2" diff --git a/test/versioned/express-esm/package.json b/test/versioned/express-esm/package.json index 1ba1fd8d82..26468c5c29 100644 --- a/test/versioned/express-esm/package.json +++ b/test/versioned/express-esm/package.json @@ -7,7 +7,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { "express": ">=4.6.0", diff --git a/test/versioned/express/package.json b/test/versioned/express/package.json index b3dab77f66..083fbd9005 100644 --- a/test/versioned/express/package.json +++ b/test/versioned/express/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "express": ">=4.6.0", diff --git a/test/versioned/fastify/package.json b/test/versioned/fastify/package.json index 0cd89fa9c1..a088c18f66 100644 --- a/test/versioned/fastify/package.json +++ b/test/versioned/fastify/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "fastify": ">=2.0.0 < 3" @@ -22,7 +22,7 @@ }, { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "fastify": ">=3.0.0", @@ -40,6 +40,6 @@ } ], "engines": { - "node": ">=16" + "node": ">=18" } } diff --git a/test/versioned/generic-pool/basic-v2.tap.js b/test/versioned/generic-pool/basic-v2.tap.js deleted file mode 100644 index 01b29b908f..0000000000 --- a/test/versioned/generic-pool/basic-v2.tap.js +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const helper = require('../../lib/agent_helper') -const tap = require('tap') - -tap.test('generic-pool', function (t) { - t.autoend() - - let agent = null - let pool = null - const PoolClass = require('generic-pool').Pool - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - pool = require('generic-pool') - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - pool = null - }) - - const tasks = [] - const decontextInterval = setInterval(function () { - if (tasks.length > 0) { - const fn = tasks.pop() - fn() - } - }, 10) - - t.teardown(function () { - clearInterval(decontextInterval) - }) - - function addTask(cb, args) { - // in versions 2.5.2 and below - // destroy tasks do not pass a callback - // so let's not add a task if cb is undefined - if (!cb) { - return - } - tasks.push(function () { - return cb.apply(null, args || []) - }) - } - - function id(tx) { - return tx && tx.id - } - - t.test('instantiation', function (t) { - t.plan(4) - - t.doesNotThrow(function () { - // eslint-disable-next-line new-cap - const p = pool.Pool({ - create: function (cb) { - addTask(cb, [null, {}]) - }, - destroy: function (o, cb) { - addTask(cb) - } - }) - t.type(p, PoolClass, 'should create a Pool') - }, 'should be able to instantiate without new') - - t.doesNotThrow(function () { - const p = new pool.Pool({ - create: function (cb) { - addTask(cb, [null, {}]) - }, - destroy: function (o, cb) { - addTask(cb) - } - }) - t.type(p, PoolClass, 'should create a Pool') - }, 'should be able to instantiate with new') - }) - - t.test('context maintenance', function (t) { - const p = new pool.Pool({ - max: 2, - min: 0, - create: function (cb) { - addTask(cb, [null, {}]) - }, - destroy: function (o, cb) { - addTask(cb) - } - }) - - Array.from({ length: 6 }, async (_, i) => { - await run(i) - }) - - drain() - - async function run(n) { - return helper.runInTransaction(agent, async (tx) => { - return new Promise((resolve, reject) => { - p.acquire((err, conn) => { - if (err) { - reject(err) - } - - t.equal(id(agent.getTransaction()), id(tx), n + ': should maintain tx state') - addTask(() => { - p.release(conn) - resolve() - }) - }) - }) - }) - } - - function drain() { - run('drain') - - helper.runInTransaction(agent, function (tx) { - p.drain(function () { - t.equal(id(agent.getTransaction()), id(tx), 'should have context through drain') - - p.destroyAllNow(function () { - t.equal(id(agent.getTransaction()), id(tx), 'should have context through destroy') - t.end() - }) - }) - }) - } - }) -}) diff --git a/test/versioned/generic-pool/package.json b/test/versioned/generic-pool/package.json index 8be18dc6db..535852d8ab 100644 --- a/test/versioned/generic-pool/package.json +++ b/test/versioned/generic-pool/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "generic-pool": ">=3.0.0" @@ -14,17 +14,6 @@ "files": [ "basic.tap.js" ] - }, - { - "engines": { - "node": ">=16" - }, - "dependencies": { - "generic-pool": ">=2.4 <3" - }, - "files": [ - "basic-v2.tap.js" - ] } ], "dependencies": {} diff --git a/test/versioned/grpc-esm/package.json b/test/versioned/grpc-esm/package.json index 1d13def8d0..c00003074c 100644 --- a/test/versioned/grpc-esm/package.json +++ b/test/versioned/grpc-esm/package.json @@ -7,7 +7,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { "@grpc/grpc-js": ">=1.4.0" diff --git a/test/versioned/grpc/package.json b/test/versioned/grpc/package.json index 97fd1c7dfb..14d90bc51b 100644 --- a/test/versioned/grpc/package.json +++ b/test/versioned/grpc/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "@grpc/grpc-js": ">=1.4.0 <1.8.0" @@ -24,7 +24,7 @@ }, { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "@grpc/grpc-js": ">=1.8.0" diff --git a/test/versioned/hapi/package.json b/test/versioned/hapi/package.json index 89d262a440..b184661593 100644 --- a/test/versioned/hapi/package.json +++ b/test/versioned/hapi/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "ejs": "2.5.5", diff --git a/test/versioned/ioredis/ioredis-3.tap.js b/test/versioned/ioredis/ioredis-3.tap.js deleted file mode 100644 index 825effdc66..0000000000 --- a/test/versioned/ioredis/ioredis-3.tap.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeMatchedModules } = require('../../lib/cache-buster') -require('../../lib/metrics_helper') -const params = require('../../lib/params') -const urltils = require('../../../lib/util/urltils') - -// Indicates unique database in Redis. 0-15 supported. -const DB_INDEX = 4 - -tap.test('ioredis instrumentation', function (t) { - let agent - let redisClient - let METRIC_HOST_NAME - let HOST_ID - - t.beforeEach(async function () { - const result = await setup(t) - agent = result.agent - redisClient = result.client - METRIC_HOST_NAME = urltils.isLocalhost(params.redis_host) - ? agent.config.getHostnameSafe() - : params.redis_host - HOST_ID = METRIC_HOST_NAME + '/' + params.redis_port - }) - - t.afterEach(function () { - agent && helper.unloadAgent(agent) - redisClient && redisClient.disconnect() - }) - - t.test('creates expected metrics', { timeout: 5000 }, function (t) { - const onError = function (error) { - return t.fail(error) - } - - agent.on('transactionFinished', function (tx) { - const expected = [ - [{ name: 'Datastore/all' }], - [{ name: 'Datastore/Redis/all' }], - [{ name: 'Datastore/operation/Redis/set' }] - ] - expected['Datastore/instance/Redis/' + HOST_ID] = 2 - t.assertMetrics(tx.metrics, expected, false, false) - t.end() - }) - - helper.runInTransaction(agent, function transactionInScope(transaction) { - redisClient - .set('testkey', 'testvalue') - .then(function () { - transaction.end() - }, onError) - .catch(onError) - }) - }) - - t.test('creates expected segments', { timeout: 5000 }, function (t) { - const onError = function (error) { - return t.fail(error) - } - - agent.on('transactionFinished', function (tx) { - const root = tx.trace.root - t.equal(root.children.length, 2, 'root has two children') - - const setSegment = root.children[0] - t.equal(setSegment.name, 'Datastore/operation/Redis/set') - - // ioredis operations return promise, any 'then' callbacks will be sibling segments - // of the original redis call - const getSegment = root.children[1] - t.equal(getSegment.name, 'Datastore/operation/Redis/get') - t.equal(getSegment.children.length, 0, 'should not contain any segments') - - t.end() - }) - - helper.runInTransaction(agent, function transactionInScope(transaction) { - redisClient - .set('testkey', 'testvalue') - .then(function () { - return redisClient.get('testkey') - }) - .then(function () { - transaction.end() - }) - .catch(onError) - }) - }) - - // NODE-1524 regression - t.test('does not crash when ending out of transaction', function (t) { - helper.runInTransaction(agent, function transactionInScope(transaction) { - t.ok(agent.getTransaction(), 'transaction should be in progress') - redisClient.set('testkey', 'testvalue').then(function () { - t.notOk(agent.getTransaction(), 'transaction should have ended') - t.end() - }) - transaction.end() - }) - }) - - t.autoend() -}) - -async function setup(t) { - const agent = helper.instrumentMockedAgent() - - // remove from cache, so that the bluebird library that ioredis uses gets - // re-instrumented - clearLoadedModules(t) - - const Redis = require('ioredis') - - const client = new Redis(params.redis_port, params.redis_host) - await helper.flushRedisDb(client, DB_INDEX) - - return new Promise(async (resolve, reject) => { - client.select(DB_INDEX, (err) => { - if (err) { - return reject(err) - } - - resolve({ agent, client }) - }) - }) -} - -function clearLoadedModules(t) { - const deletedCount = removeMatchedModules(/ioredis\/node_modules\/ioredis/) - t.comment(`Cleared ${deletedCount} modules matching '*/ioredis/node_modules/ioredis/*'`) -} diff --git a/test/versioned/ioredis/package.json b/test/versioned/ioredis/package.json index d1e4af8114..7513fa3e48 100644 --- a/test/versioned/ioredis/package.json +++ b/test/versioned/ioredis/package.json @@ -6,18 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" - }, - "dependencies": { - "ioredis": "3.x" - }, - "files": [ - "ioredis-3.tap.js" - ] - }, - { - "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "ioredis": ">=4.0.0" diff --git a/test/versioned/kafkajs/package.json b/test/versioned/kafkajs/package.json index 00d51e92c1..1c13a78547 100644 --- a/test/versioned/kafkajs/package.json +++ b/test/versioned/kafkajs/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "kafkajs": ">=2.0.0" diff --git a/test/versioned/koa/package.json b/test/versioned/koa/package.json index 2553dbadb3..50a1775d0f 100644 --- a/test/versioned/koa/package.json +++ b/test/versioned/koa/package.json @@ -11,7 +11,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "koa": { @@ -26,7 +26,7 @@ }, { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "koa": { @@ -34,7 +34,7 @@ "samples": 5 }, "koa-router": { - "versions": ">=7.1.0", + "versions": ">=11.0.2", "samples": 5 } }, @@ -45,7 +45,7 @@ }, { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "koa": { @@ -53,7 +53,7 @@ "samples": 5 }, "@koa/router": { - "versions": ">=8.0.0", + "versions": ">=11.0.2", "samples": 5 } }, @@ -64,7 +64,7 @@ }, { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "koa": { diff --git a/test/versioned/memcached/package.json b/test/versioned/memcached/package.json index 60429c402c..82adb5450c 100644 --- a/test/versioned/memcached/package.json +++ b/test/versioned/memcached/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "memcached": ">=2.2.0" diff --git a/test/versioned/mongodb-esm/bulk.tap.mjs b/test/versioned/mongodb-esm/bulk.tap.mjs index 5655103297..3dfed0d911 100644 --- a/test/versioned/mongodb-esm/bulk.tap.mjs +++ b/test/versioned/mongodb-esm/bulk.tap.mjs @@ -4,81 +4,76 @@ */ import tap from 'tap' -import semver from 'semver' import { test } from './collection-common.mjs' import helper from '../../lib/agent_helper.js' -import { pkgVersion } from './common.cjs' import { STATEMENT_PREFIX } from './common.cjs' -// see test/versioned/mongodb/common.js -if (semver.satisfies(pkgVersion, '>=3.2.4 <4.1.4')) { - tap.test('Bulk operations', (t) => { - t.autoend() - let agent +tap.test('Bulk operations', (t) => { + t.autoend() + let agent - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) + t.before(() => { + agent = helper.instrumentMockedAgent() + }) - test( - { suiteName: 'unorderedBulkOp', agent, t }, - function unorderedBulkOpTest(t, collection, verify) { - const bulk = collection.initializeUnorderedBulkOp() - bulk - .find({ - i: 1 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - bulk - .find({ - i: 2 - }) - .updateOne({ - $set: { foo: 'bar' } - }) + t.teardown(() => { + helper.unloadAgent(agent) + }) - bulk.execute(function done(err) { - t.error(err) - verify( - null, - [`${STATEMENT_PREFIX}/unorderedBulk/batch`, 'Callback: done'], - ['unorderedBulk'] - ) + test( + { suiteName: 'unorderedBulkOp', agent, t }, + function unorderedBulkOpTest(t, collection, verify) { + const bulk = collection.initializeUnorderedBulkOp() + bulk + .find({ + i: 1 + }) + .updateOne({ + $set: { foo: 'bar' } + }) + bulk + .find({ + i: 2 + }) + .updateOne({ + $set: { foo: 'bar' } }) - } - ) - test( - { suiteName: 'orderedBulkOp', agent, t }, - function unorderedBulkOpTest(t, collection, verify) { - const bulk = collection.initializeOrderedBulkOp() - bulk - .find({ - i: 1 - }) - .updateOne({ - $set: { foo: 'bar' } - }) + bulk.execute(function done(err) { + t.error(err) + verify( + null, + [`${STATEMENT_PREFIX}/unorderedBulk/batch`, 'Callback: done'], + ['unorderedBulk'] + ) + }) + } + ) - bulk - .find({ - i: 2 - }) - .updateOne({ - $set: { foo: 'bar' } - }) + test( + { suiteName: 'orderedBulkOp', agent, t }, + function unorderedBulkOpTest(t, collection, verify) { + const bulk = collection.initializeOrderedBulkOp() + bulk + .find({ + i: 1 + }) + .updateOne({ + $set: { foo: 'bar' } + }) - bulk.execute(function done(err) { - t.error(err) - verify(null, [`${STATEMENT_PREFIX}/orderedBulk/batch`, 'Callback: done'], ['orderedBulk']) + bulk + .find({ + i: 2 }) - } - ) - }) -} + .updateOne({ + $set: { foo: 'bar' } + }) + + bulk.execute(function done(err) { + t.error(err) + verify(null, [`${STATEMENT_PREFIX}/orderedBulk/batch`, 'Callback: done'], ['orderedBulk']) + }) + } + ) +}) diff --git a/test/versioned/mongodb-esm/collection-find.tap.mjs b/test/versioned/mongodb-esm/collection-find.tap.mjs index c78c14ad0b..964df3313f 100644 --- a/test/versioned/mongodb-esm/collection-find.tap.mjs +++ b/test/versioned/mongodb-esm/collection-find.tap.mjs @@ -3,17 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import semver from 'semver' import tap from 'tap' import { test } from './collection-common.mjs' import helper from '../../lib/agent_helper.js' -import { pkgVersion, STATEMENT_PREFIX } from './common.cjs' - -let findOpt = { returnOriginal: false } -// 4.0.0 changed this opt https://github.com/mongodb/node-mongodb-native/pull/2803/files -if (semver.satisfies(pkgVersion, '>=4')) { - findOpt = { returnDocument: 'after' } -} +import { STATEMENT_PREFIX } from './common.cjs' +const findOpt = { returnDocument: 'after' } tap.test('Collection(Find) Tests', (t) => { t.autoend() @@ -27,35 +21,6 @@ tap.test('Collection(Find) Tests', (t) => { helper.unloadAgent(agent) }) - if (semver.satisfies(pkgVersion, '<4')) { - test( - { suiteName: 'findAndModify', agent, t }, - function findAndModifyTest(t, collection, verify) { - collection.findAndModify({ i: 1 }, [['i', 1]], { $set: { a: 15 } }, { new: true }, done) - - function done(err, data) { - t.error(err) - t.equal(data.value.a, 15) - t.equal(data.value.i, 1) - t.equal(data.ok, 1) - verify(null, [`${STATEMENT_PREFIX}/findAndModify`, 'Callback: done'], ['findAndModify']) - } - } - ) - - test( - { suiteName: 'findAndRemove', agent, t }, - function findAndRemoveTest(t, collection, verify) { - collection.findAndRemove({ i: 1 }, [['i', 1]], function done(err, data) { - t.error(err) - t.equal(data.value.i, 1) - t.equal(data.ok, 1) - verify(null, [`${STATEMENT_PREFIX}/findAndRemove`, 'Callback: done'], ['findAndRemove']) - }) - } - ) - } - test({ suiteName: 'findOne', agent, t }, function findOneTest(t, collection, verify) { collection.findOne({ i: 15 }, function done(err, data) { t.error(err) diff --git a/test/versioned/mongodb-esm/collection-index.tap.mjs b/test/versioned/mongodb-esm/collection-index.tap.mjs index 465d3b983b..6e9e3fb52e 100644 --- a/test/versioned/mongodb-esm/collection-index.tap.mjs +++ b/test/versioned/mongodb-esm/collection-index.tap.mjs @@ -3,11 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import semver from 'semver' import tap from 'tap' -import { test, DB_NAME } from './collection-common.mjs' +import { test } from './collection-common.mjs' import helper from '../../lib/agent_helper.js' -import { pkgVersion, STATEMENT_PREFIX, COLLECTIONS } from './common.cjs' +import { STATEMENT_PREFIX } from './common.cjs' tap.test('Collection(Index) Tests', (t) => { t.autoend() @@ -58,13 +57,6 @@ tap.test('Collection(Index) Tests', (t) => { name: '_id_' } - // this will fail if running a mongodb server > 4.3.1 - // https://jira.mongodb.org/browse/SERVER-41696 - // we only connect to a server > 4.3.1 when using the mongodb - // driver of 4.2.0+ - if (semver.satisfies(pkgVersion, '<4.2.0')) { - expectedResult.ns = `${DB_NAME}.${COLLECTIONS.collection1}` - } t.same(result, expectedResult, 'should have expected results') verify(null, [`${STATEMENT_PREFIX}/indexes`, 'Callback: done'], ['indexes']) @@ -95,34 +87,4 @@ tap.test('Collection(Index) Tests', (t) => { }) } ) - - if (semver.satisfies(pkgVersion, '<4')) { - test( - { suiteName: 'dropAllIndexes', agent, t }, - function dropAllIndexesTest(t, collection, verify) { - collection.dropAllIndexes(function done(err, data) { - t.error(err) - t.equal(data, true) - verify(null, [`${STATEMENT_PREFIX}/dropAllIndexes`, 'Callback: done'], ['dropAllIndexes']) - }) - } - ) - - test({ suiteName: 'ensureIndex', agent, t }, function ensureIndexTest(t, collection, verify) { - collection.ensureIndex('i', function done(err, data) { - t.error(err) - t.equal(data, 'i_1') - verify(null, [`${STATEMENT_PREFIX}/ensureIndex`, 'Callback: done'], ['ensureIndex']) - }) - }) - - test({ suiteName: 'reIndex', agent, t }, function reIndexTest(t, collection, verify) { - collection.reIndex(function done(err, data) { - t.error(err) - t.equal(data, true) - - verify(null, [`${STATEMENT_PREFIX}/reIndex`, 'Callback: done'], ['reIndex']) - }) - }) - } }) diff --git a/test/versioned/mongodb-esm/collection-misc.tap.mjs b/test/versioned/mongodb-esm/collection-misc.tap.mjs index 4c47854e40..f6388f151f 100644 --- a/test/versioned/mongodb-esm/collection-misc.tap.mjs +++ b/test/versioned/mongodb-esm/collection-misc.tap.mjs @@ -3,11 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import semver from 'semver' import tap from 'tap' import { test, DB_NAME } from './collection-common.mjs' import helper from '../../lib/agent_helper.js' -import { pkgVersion, STATEMENT_PREFIX, COLLECTIONS } from './common.cjs' +import { STATEMENT_PREFIX, COLLECTIONS } from './common.cjs' function verifyAggregateData(t, data) { t.equal(data.length, 3, 'should have expected amount of results') @@ -26,47 +25,26 @@ tap.test('Collection(Index) Tests', (t) => { helper.unloadAgent(agent) }) - if (semver.satisfies(pkgVersion, '<4')) { - test({ suiteName: 'aggregate', agent, t }, function aggregateTest(t, collection, verify) { - const cursor = collection.aggregate([ - { $sort: { i: 1 } }, - { $match: { mod10: 5 } }, - { $limit: 3 }, - { $project: { value: '$i', _id: 0 } } - ]) - - cursor.toArray(function onResult(err, data) { - verifyAggregateData(t, data) - verify( - err, - [`${STATEMENT_PREFIX}/aggregate`, `${STATEMENT_PREFIX}/toArray`], - ['aggregate', 'toArray'], - { childrenLength: 2, strict: false } - ) - }) - }) - } else { - test( - { suiteName: 'aggregate v4', agent, t }, - async function aggregateTest(t, collection, verify) { - const data = await collection - .aggregate([ - { $sort: { i: 1 } }, - { $match: { mod10: 5 } }, - { $limit: 3 }, - { $project: { value: '$i', _id: 0 } } - ]) - .toArray() - verifyAggregateData(t, data) - verify( - null, - [`${STATEMENT_PREFIX}/aggregate`, `${STATEMENT_PREFIX}/toArray`], - ['aggregate', 'toArray'], - { childrenLength: 2 } - ) - } - ) - } + test( + { suiteName: 'aggregate v4', agent, t }, + async function aggregateTest(t, collection, verify) { + const data = await collection + .aggregate([ + { $sort: { i: 1 } }, + { $match: { mod10: 5 } }, + { $limit: 3 }, + { $project: { value: '$i', _id: 0 } } + ]) + .toArray() + verifyAggregateData(t, data) + verify( + null, + [`${STATEMENT_PREFIX}/aggregate`, `${STATEMENT_PREFIX}/toArray`], + ['aggregate', 'toArray'], + { childrenLength: 2 } + ) + } + ) test({ suiteName: 'bulkWrite', agent, t }, function bulkWriteTest(t, collection, verify) { collection.bulkWrite( @@ -107,39 +85,6 @@ tap.test('Collection(Index) Tests', (t) => { }) }) - if (semver.satisfies(pkgVersion, '<3')) { - test({ suiteName: 'geoNear', agent, t }, function geoNearTest(t, collection, verify) { - collection.ensureIndex({ loc: '2d' }, { bucketSize: 1 }, indexed) - - function indexed(err) { - t.error(err) - collection.geoNear(20, 20, { maxDistance: 5 }, done) - } - - function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - t.equal(data.results.length, 2) - t.equal(data.results[0].obj.i, 21) - t.equal(data.results[1].obj.i, 17) - t.same(data.results[0].obj.loc, [21, 21]) - t.same(data.results[1].obj.loc, [17, 17]) - t.equal(data.results[0].dis, 1.4142135623730951) - t.equal(data.results[1].dis, 4.242640687119285) - verify( - null, - [ - `${STATEMENT_PREFIX}/ensureIndex`, - 'Callback: indexed', - `${STATEMENT_PREFIX}/geoNear`, - 'Callback: done' - ], - ['ensureIndex', 'geoNear'] - ) - } - }) - } - test({ suiteName: 'isCapped', agent, t }, function isCappedTest(t, collection, verify) { collection.isCapped(function done(err, data) { t.error(err) @@ -203,97 +148,6 @@ tap.test('Collection(Index) Tests', (t) => { }) }) - if (semver.satisfies(pkgVersion, '<4')) { - test({ suiteName: 'parallelCollectionScan', agent, t }, function (t, collection, verify) { - collection.parallelCollectionScan({ numCursors: 1 }, function done(err, cursors) { - t.error(err) - - cursors[0].toArray(function toArray(err, items) { - t.error(err) - t.equal(items.length, 30) - - const total = items.reduce(function sum(prev, item) { - return item.i + prev - }, 0) - - t.equal(total, 435) - verify( - null, - [ - `${STATEMENT_PREFIX}/parallelCollectionScan`, - 'Callback: done', - `${STATEMENT_PREFIX}/toArray`, - 'Callback: toArray' - ], - ['parallelCollectionScan', 'toArray'] - ) - }) - }) - }) - - test( - { suiteName: 'geoHaystackSearch', agent, t }, - function haystackSearchTest(t, collection, verify) { - collection.ensureIndex({ loc: 'geoHaystack', type: 1 }, { bucketSize: 1 }, indexed) - - function indexed(err) { - t.error(err) - collection.geoHaystackSearch(15, 15, { maxDistance: 5, search: {} }, done) - } - - function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - t.equal(data.results.length, 2) - t.equal(data.results[0].i, 13) - t.equal(data.results[1].i, 17) - t.same(data.results[0].loc, [13, 13]) - t.same(data.results[1].loc, [17, 17]) - verify( - null, - [ - `${STATEMENT_PREFIX}/ensureIndex`, - 'Callback: indexed', - `${STATEMENT_PREFIX}/geoHaystackSearch`, - 'Callback: done' - ], - ['ensureIndex', 'geoHaystackSearch'] - ) - } - } - ) - - test({ suiteName: 'group', agent, t }, function groupTest(t, collection, verify) { - collection.group(['mod10'], {}, { count: 0, total: 0 }, count, done) - - function done(err, data) { - t.error(err) - t.same(data.sort(sort), [ - { mod10: 0, count: 3, total: 30 }, - { mod10: 1, count: 3, total: 33 }, - { mod10: 2, count: 3, total: 36 }, - { mod10: 3, count: 3, total: 39 }, - { mod10: 4, count: 3, total: 42 }, - { mod10: 5, count: 3, total: 45 }, - { mod10: 6, count: 3, total: 48 }, - { mod10: 7, count: 3, total: 51 }, - { mod10: 8, count: 3, total: 54 }, - { mod10: 9, count: 3, total: 57 } - ]) - verify(null, [`${STATEMENT_PREFIX}/group`, 'Callback: done'], ['group']) - } - - function count(obj, prev) { - prev.total += obj.i - prev.count++ - } - - function sort(a, b) { - return a.mod10 - b.mod10 - } - }) - } - test({ suiteName: 'rename', agent, t }, function renameTest(t, collection, verify) { collection.rename(COLLECTIONS.collection2, function done(err) { t.error(err) diff --git a/test/versioned/mongodb-esm/collection-update.tap.mjs b/test/versioned/mongodb-esm/collection-update.tap.mjs index fbe6594d9b..845ea08f82 100644 --- a/test/versioned/mongodb-esm/collection-update.tap.mjs +++ b/test/versioned/mongodb-esm/collection-update.tap.mjs @@ -3,11 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import semver from 'semver' import tap from 'tap' import { test } from './collection-common.mjs' import helper from '../../lib/agent_helper.js' -import { pkgVersion, STATEMENT_PREFIX } from './common.cjs' +import { STATEMENT_PREFIX } from './common.cjs' /** * The response from the methods in this file differ between versions @@ -19,22 +18,13 @@ import { pkgVersion, STATEMENT_PREFIX } from './common.cjs' * @param {Number} params.count, optional * @param {string} params.keyPrefix prefix where the count exists * @param {Object} params.extraValues extra fields to assert on >=4.0.0 version of module - * @param {Object} params.legaycValues extra fields to assert on <4.0.0 version of module */ -function assertExpectedResult({ t, data, count, keyPrefix, extraValues, legacyValues }) { - if (semver.satisfies(pkgVersion, '<4')) { - const expectedResult = { ok: 1, ...legacyValues } - if (count) { - expectedResult.n = count - } - t.same(data.result, expectedResult) - } else { - const expectedResult = { acknowledged: true, ...extraValues } - if (count) { - expectedResult[`${keyPrefix}Count`] = count - } - t.same(data, expectedResult) +function assertExpectedResult({ t, data, count, keyPrefix, extraValues }) { + const expectedResult = { acknowledged: true, ...extraValues } + if (count) { + expectedResult[`${keyPrefix}Count`] = count } + t.same(data, expectedResult) } tap.test('Collection(Update) Tests', (t) => { @@ -120,9 +110,6 @@ tap.test('Collection(Update) Tests', (t) => { assertExpectedResult({ t, data, - legacyValues: { - n: 1 - }, extraValues: { insertedId: {} } @@ -154,9 +141,6 @@ tap.test('Collection(Update) Tests', (t) => { data, count: 1, keyPrefix: 'modified', - legacyValues: { - nModified: 1 - }, extraValues: { matchedCount: 1, upsertedCount: 0, @@ -168,17 +152,6 @@ tap.test('Collection(Update) Tests', (t) => { }) }) - if (semver.satisfies(pkgVersion, '<4')) { - test({ suiteName: 'save', agent, t }, function saveTest(t, collection, verify) { - collection.save({ foo: 'bar' }, function done(err, data) { - t.error(err) - t.same(data.result, { ok: 1, n: 1 }) - - verify(null, [`${STATEMENT_PREFIX}/save`, 'Callback: done'], ['save']) - }) - }) - } - test({ suiteName: 'update', agent, t }, function updateTest(t, collection, verify) { collection.update({ i: 5 }, { $set: { foo: 'bar' } }, function done(err, data) { t.error(err) @@ -187,9 +160,6 @@ tap.test('Collection(Update) Tests', (t) => { data, count: 1, keyPrefix: 'modified', - legacyValues: { - nModified: 1 - }, extraValues: { matchedCount: 1, upsertedCount: 0, @@ -209,9 +179,6 @@ tap.test('Collection(Update) Tests', (t) => { data, count: 3, keyPrefix: 'modified', - legacyValues: { - nModified: 3 - }, extraValues: { matchedCount: 3, upsertedCount: 0, @@ -231,9 +198,6 @@ tap.test('Collection(Update) Tests', (t) => { data, count: 1, keyPrefix: 'modified', - legacyValues: { - nModified: 1 - }, extraValues: { matchedCount: 1, upsertedCount: 0, diff --git a/test/versioned/mongodb-esm/common.cjs b/test/versioned/mongodb-esm/common.cjs index a28f7f0afe..487a888507 100644 --- a/test/versioned/mongodb-esm/common.cjs +++ b/test/versioned/mongodb-esm/common.cjs @@ -4,8 +4,9 @@ */ 'use strict' +module.exports = require('../mongodb/common') -const fs = require('fs') +/* const fs = require('fs') const mongoPackage = require('mongodb/package.json') const params = require('../../lib/params') const semver = require('semver') @@ -226,3 +227,4 @@ function getDomainSocketPath() { function getMetrics(agent) { return agent.metrics._metrics } +*/ diff --git a/test/versioned/mongodb-esm/package.json b/test/versioned/mongodb-esm/package.json index 02476a7496..d5ac6b5dbf 100644 --- a/test/versioned/mongodb-esm/package.json +++ b/test/versioned/mongodb-esm/package.json @@ -6,11 +6,12 @@ "private": true, "tests": [ { + "comment": "Only tests callback based instrumentation which is only in v4 of mongodb. For promised-based v5+ tests see `test/versioned/mongodb`", "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { - "mongodb": ">=2.1 < 4.0.0 || >= 4.1.4 < 5" + "mongodb": ">=4.1.4 <5" }, "files": [ "bulk.tap.mjs", diff --git a/test/versioned/mongodb/collection-common.js b/test/versioned/mongodb/collection-common.js index 73417bd600..6f18c96e5b 100644 --- a/test/versioned/mongodb/collection-common.js +++ b/test/versioned/mongodb/collection-common.js @@ -8,8 +8,6 @@ const common = require('./common') const tap = require('tap') const helper = require('../../lib/agent_helper') -const semver = require('semver') -const { version: pkgVersion } = require('mongodb/package') let METRIC_HOST_NAME = null let METRIC_HOST_PORT = null @@ -184,104 +182,100 @@ function collectionTest(name, run) { }) }) - // this seems to break in 3.x up to 3.6.0 - // I think it is because of this https://jira.mongodb.org/browse/NODE-2452 - if (semver.satisfies(pkgVersion, '>=3.6.0')) { - t.test('replica set string remote connection', function (t) { - t.autoend() - t.beforeEach(async function () { - agent = helper.instrumentMockedAgent() - - const mongodb = require('mongodb') - - await dropTestCollections(mongodb) - METRIC_HOST_NAME = common.getHostName(agent) - METRIC_HOST_PORT = common.getPort() - const res = await common.connect(mongodb, null, true) - client = res.client - db = res.db - collection = db.collection(COLLECTIONS.collection1) - await populate(collection) - }) + t.test('replica set string remote connection', function (t) { + t.autoend() + t.beforeEach(async function () { + agent = helper.instrumentMockedAgent() - t.afterEach(async function () { - await common.close(client, db) - helper.unloadAgent(agent) - agent = null - }) + const mongodb = require('mongodb') - t.test('should generate the correct metrics and segments', function (t) { - helper.runInTransaction(agent, function (transaction) { - transaction.name = common.TRANSACTION_NAME - run( - t, - collection, - function (err, segments, metrics, { childrenLength = 1, strict = true } = {}) { - if ( - !t.error(err, 'running test should not error') || - !t.ok(agent.getTransaction(), 'should maintain tx state') - ) { - return t.end() - } - t.equal(agent.getTransaction().id, transaction.id, 'should not change transactions') - const segment = agent.tracer.getSegment() - let current = transaction.trace.root - - // this logic is just for the collection.aggregate. - // aggregate no longer returns a callback with cursor - // it just returns a cursor. so the segments on the - // transaction are not nested but both on the trace - // root. instead of traversing the children, just - // iterate over the expected segments and compare - // against the corresponding child on trace root - // we also added a strict flag for aggregate because depending on the version - // there is an extra segment for the callback of our test which we do not care - // to assert - if (childrenLength === 2) { - t.equal(current.children.length, childrenLength, 'should have one child') + await dropTestCollections(mongodb) + METRIC_HOST_NAME = common.getHostName(agent) + METRIC_HOST_PORT = common.getPort() + const res = await common.connect(mongodb, null, true) + client = res.client + db = res.db + collection = db.collection(COLLECTIONS.collection1) + await populate(collection) + }) + + t.afterEach(async function () { + await common.close(client, db) + helper.unloadAgent(agent) + agent = null + }) - segments.forEach((expectedSegment, i) => { - const child = current.children[i] - - t.equal(child.name, expectedSegment, `child should be named ${expectedSegment}`) - if (common.MONGO_SEGMENT_RE.test(child.name)) { - checkSegmentParams(t, child) - t.equal(child.ignore, false, 'should not ignore segment') - } - - if (strict) { - t.equal(child.children.length, 0, 'should have no more children') - } - }) - } else { - for (let i = 0, l = segments.length; i < l; ++i) { - t.equal(current.children.length, childrenLength, 'should have one child') - current = current.children[0] - t.equal(current.name, segments[i], 'child should be named ' + segments[i]) - if (common.MONGO_SEGMENT_RE.test(current.name)) { - checkSegmentParams(t, current) - t.equal(current.ignore, false, 'should not ignore segment') - } + t.test('should generate the correct metrics and segments', function (t) { + helper.runInTransaction(agent, function (transaction) { + transaction.name = common.TRANSACTION_NAME + run( + t, + collection, + function (err, segments, metrics, { childrenLength = 1, strict = true } = {}) { + if ( + !t.error(err, 'running test should not error') || + !t.ok(agent.getTransaction(), 'should maintain tx state') + ) { + return t.end() + } + t.equal(agent.getTransaction().id, transaction.id, 'should not change transactions') + const segment = agent.tracer.getSegment() + let current = transaction.trace.root + + // this logic is just for the collection.aggregate. + // aggregate no longer returns a callback with cursor + // it just returns a cursor. so the segments on the + // transaction are not nested but both on the trace + // root. instead of traversing the children, just + // iterate over the expected segments and compare + // against the corresponding child on trace root + // we also added a strict flag for aggregate because depending on the version + // there is an extra segment for the callback of our test which we do not care + // to assert + if (childrenLength === 2) { + t.equal(current.children.length, childrenLength, 'should have one child') + + segments.forEach((expectedSegment, i) => { + const child = current.children[i] + + t.equal(child.name, expectedSegment, `child should be named ${expectedSegment}`) + if (common.MONGO_SEGMENT_RE.test(child.name)) { + checkSegmentParams(t, child) + t.equal(child.ignore, false, 'should not ignore segment') } if (strict) { - t.equal(current.children.length, 0, 'should have no more children') + t.equal(child.children.length, 0, 'should have no more children') + } + }) + } else { + for (let i = 0, l = segments.length; i < l; ++i) { + t.equal(current.children.length, childrenLength, 'should have one child') + current = current.children[0] + t.equal(current.name, segments[i], 'child should be named ' + segments[i]) + if (common.MONGO_SEGMENT_RE.test(current.name)) { + checkSegmentParams(t, current) + t.equal(current.ignore, false, 'should not ignore segment') } } if (strict) { - t.ok(current === segment, 'should test to the current segment') + t.equal(current.children.length, 0, 'should have no more children') } + } - transaction.end() - common.checkMetrics(t, agent, METRIC_HOST_NAME, METRIC_HOST_PORT, metrics || []) - t.end() + if (strict) { + t.ok(current === segment, 'should test to the current segment') } - ) - }) + + transaction.end() + common.checkMetrics(t, agent, METRIC_HOST_NAME, METRIC_HOST_PORT, metrics || []) + t.end() + } + ) }) }) - } + }) }) } diff --git a/test/versioned/mongodb/collection-index.tap.js b/test/versioned/mongodb/collection-index.tap.js index 6014a9cc66..0576063696 100644 --- a/test/versioned/mongodb/collection-index.tap.js +++ b/test/versioned/mongodb/collection-index.tap.js @@ -6,8 +6,7 @@ 'use strict' const common = require('./collection-common') -const semver = require('semver') -const { COLLECTIONS, DB_NAME, pkgVersion, STATEMENT_PREFIX } = require('./common') +const { STATEMENT_PREFIX } = require('./common') common.test('createIndex', async function createIndexTest(t, collection, verify) { const data = await collection.createIndex('i') @@ -36,14 +35,6 @@ common.test('indexes', async function indexesTest(t, collection, verify) { name: '_id_' } - // this will fail if running a mongodb server > 4.3.1 - // https://jira.mongodb.org/browse/SERVER-41696 - // we only connect to a server > 4.3.1 when using the mongodb - // driver of 4.2.0+ - if (semver.satisfies(pkgVersion, '<4.2.0')) { - expectedResult.ns = `${DB_NAME}.${COLLECTIONS.collection1}` - } - t.same(result, expectedResult, 'should have expected results') verify(null, [`${STATEMENT_PREFIX}/indexes`], ['indexes'], { strict: false }) diff --git a/test/versioned/mongodb/common.js b/test/versioned/mongodb/common.js index 256d94db16..d4dd44fb22 100644 --- a/test/versioned/mongodb/common.js +++ b/test/versioned/mongodb/common.js @@ -7,7 +7,6 @@ const mongoPackage = require('mongodb/package.json') const params = require('../../lib/params') -const semver = require('semver') const urltils = require('../../../lib/util/urltils') const MONGO_SEGMENT_RE = /^Datastore\/.*?\/MongoDB/ @@ -23,87 +22,17 @@ exports.COLLECTIONS = COLLECTIONS exports.STATEMENT_PREFIX = STATEMENT_PREFIX exports.pkgVersion = mongoPackage.version -// Check package versions to decide which connect function to use below -exports.connect = function connect() { - if (semver.satisfies(mongoPackage.version, '<3')) { - return connectV2.apply(this, arguments) - } else if (semver.satisfies(mongoPackage.version, '>=3 <4.2.0')) { - return connectV3.apply(this, arguments) - } - return connectV4.apply(this, arguments) -} - -exports.close = function close() { - if (semver.satisfies(mongoPackage.version, '<4')) { - return closeLegacy.apply(this, arguments) - } - return closeAsync.apply(this, arguments) -} - +exports.connect = connect +exports.close = close exports.checkMetrics = checkMetrics exports.getHostName = getHostName exports.getPort = getPort -function connectV2(mongodb, path) { - return new Promise((resolve, reject) => { - let server = null - if (path) { - server = new mongodb.Server(path) - } else { - server = new mongodb.Server(params.mongodb_host, params.mongodb_port, { - socketOptions: { - connectionTimeoutMS: 30000, - socketTimeoutMS: 30000 - } - }) - } - - const db = new mongodb.Db(DB_NAME, server) - - db.open(function (err) { - if (err) { - reject(err) - } - - resolve({ db, client: null }) - }) - }) -} - -function connectV3(mongodb, host, replicaSet = false) { - return new Promise((resolve, reject) => { - if (host) { - host = encodeURIComponent(host) - } else { - host = params.mongodb_host + ':' + params.mongodb_port - } - - let connString = `mongodb://${host}` - let options = {} - - if (replicaSet) { - connString = `mongodb://${host},${host},${host}` - options = { useNewUrlParser: true, useUnifiedTopology: true } - } - mongodb.MongoClient.connect(connString, options, function (err, client) { - if (err) { - reject(err) - } - - const db = client.db(DB_NAME) - resolve({ db, client }) - }) - }) -} - -// This is same as connectV3 except it uses a different -// set of params to connect to the mongodb_v4 container -// it is actually just using the `mongodb:5` image -async function connectV4(mongodb, host, replicaSet = false) { +async function connect(mongodb, host, replicaSet = false) { if (host) { host = encodeURIComponent(host) } else { - host = params.mongodb_v4_host + ':' + params.mongodb_v4_port + host = params.mongodb_host + ':' + params.mongodb_port } let connString = `mongodb://${host}` @@ -118,19 +47,7 @@ async function connectV4(mongodb, host, replicaSet = false) { return { db, client } } -function closeLegacy(client, db) { - return new Promise((resolve) => { - if (db && typeof db.close === 'function') { - db.close(resolve) - } else if (client) { - client.close(true, resolve) - } else { - resolve() - } - }) -} - -async function closeAsync(client, db) { +async function close(client, db) { if (db && typeof db.close === 'function') { await db.close() } else if (client) { @@ -139,16 +56,12 @@ async function closeAsync(client, db) { } function getHostName(agent) { - const host = semver.satisfies(mongoPackage.version, '>=4.2.0') - ? params.mongodb_v4_host - : params.mongodb_host + const host = params.mongodb_host return urltils.isLocalhost(host) ? agent.config.getHostnameSafe() : host } function getPort() { - return semver.satisfies(mongoPackage.version, '>=4.2.0') - ? String(params.mongodb_v4_port) - : String(params.mongodb_port) + return String(params.mongodb_port) } function checkMetrics(t, agent, host, port, metrics) { diff --git a/test/versioned/mongodb/db-common.js b/test/versioned/mongodb/db-common.js index ee57ebf320..000db19280 100644 --- a/test/versioned/mongodb/db-common.js +++ b/test/versioned/mongodb/db-common.js @@ -5,7 +5,6 @@ 'use strict' const common = require('./common') -const semver = require('semver') const collectionCommon = require('./collection-common') const helper = require('../../lib/agent_helper') const tap = require('tap') @@ -13,9 +12,6 @@ const tap = require('tap') let MONGO_HOST = null let MONGO_PORT = null const BAD_MONGO_COMMANDS = ['collection'] -if (semver.satisfies(common.pkgVersion, '2.2.x')) { - BAD_MONGO_COMMANDS.push('authenticate', 'logout') -} function dbTest(name, run) { mongoTest(name, function init(t, agent) { diff --git a/test/versioned/mongodb/legacy/bulk.tap.js b/test/versioned/mongodb/legacy/bulk.tap.js deleted file mode 100644 index 662e9b54ea..0000000000 --- a/test/versioned/mongodb/legacy/bulk.tap.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const common = require('../collection-common') -const semver = require('semver') -const { pkgVersion, STATEMENT_PREFIX } = require('../common') - -// see test/versioned/mongodb/common.js -if (semver.satisfies(pkgVersion, '>=3.2.4')) { - common.test('unorderedBulkOp', function unorderedBulkOpTest(t, collection, verify) { - const bulk = collection.initializeUnorderedBulkOp() - bulk - .find({ - i: 1 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - bulk - .find({ - i: 2 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - - bulk.execute(function done(err) { - t.error(err) - verify(null, [`${STATEMENT_PREFIX}/unorderedBulk/batch`, 'Callback: done'], ['unorderedBulk']) - }) - }) - - common.test('orderedBulkOp', function unorderedBulkOpTest(t, collection, verify) { - const bulk = collection.initializeOrderedBulkOp() - bulk - .find({ - i: 1 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - - bulk - .find({ - i: 2 - }) - .updateOne({ - $set: { foo: 'bar' } - }) - - bulk.execute(function done(err) { - t.error(err) - verify(null, [`${STATEMENT_PREFIX}/orderedBulk/batch`, 'Callback: done'], ['orderedBulk']) - }) - }) -} diff --git a/test/versioned/mongodb/legacy/cursor.tap.js b/test/versioned/mongodb/legacy/cursor.tap.js deleted file mode 100644 index c9ebee7747..0000000000 --- a/test/versioned/mongodb/legacy/cursor.tap.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const common = require('../collection-common') -const concat = require('concat-stream') -const helper = require('../../../lib/agent_helper') -const semver = require('semver') -const tap = require('tap') -const { pkgVersion, STATEMENT_PREFIX, COLLECTIONS } = require('../common') - -common.test('count', function countTest(t, collection, verify) { - collection.find({}).count(function onCount(err, data) { - t.notOk(err, 'should not error') - t.ok(data >= 30, 'should have correct result') - verify(null, [`${STATEMENT_PREFIX}/count`, 'Callback: onCount'], ['count']) - }) -}) - -common.test('explain', function explainTest(t, collection, verify) { - collection.find({}).explain(function onExplain(err, data) { - t.error(err) - // Depending on the version of the mongo server the explain plan is different. - if (data.hasOwnProperty('cursor')) { - t.equal(data.cursor, 'BasicCursor', 'should have correct response') - } else { - t.ok(data.hasOwnProperty('queryPlanner'), 'should have correct response') - } - verify(null, [`${STATEMENT_PREFIX}/explain`, 'Callback: onExplain'], ['explain']) - }) -}) - -if (semver.satisfies(pkgVersion, '<3')) { - common.test('nextObject', function nextObjectTest(t, collection, verify) { - collection.find({}).nextObject(function onNextObject(err, data) { - t.notOk(err) - t.equal(data.i, 0) - verify(null, [`${STATEMENT_PREFIX}/nextObject`, 'Callback: onNextObject'], ['nextObject']) - }) - }) -} - -common.test('next', function nextTest(t, collection, verify) { - collection.find({}).next(function onNext(err, data) { - t.notOk(err) - t.equal(data.i, 0) - verify(null, [`${STATEMENT_PREFIX}/next`, 'Callback: onNext'], ['next']) - }) -}) - -common.test('toArray', function toArrayTest(t, collection, verify) { - collection.find({}).toArray(function onToArray(err, data) { - t.notOk(err) - t.equal(data[0].i, 0) - verify(null, [`${STATEMENT_PREFIX}/toArray`, 'Callback: onToArray'], ['toArray']) - }) -}) - -tap.test('piping cursor stream hides internal calls', function (t) { - let agent = helper.instrumentMockedAgent() - let client = null - let db = null - let collection = null - - t.teardown(async function () { - await common.close(client, db) - helper.unloadAgent(agent) - agent = null - }) - - const mongodb = require('mongodb') - common - .dropTestCollections(mongodb) - .then(() => { - return common.connect(mongodb) - }) - .then((res) => { - client = res.client - db = res.db - - collection = db.collection(COLLECTIONS.collection1) - return common.populate(collection) - }) - .then(runTest) - - function runTest() { - helper.runInTransaction(agent, function (transaction) { - transaction.name = common.TRANSACTION_NAME - const destination = concat(function () {}) - - destination.on('finish', function () { - transaction.end() - t.equal( - transaction.trace.root.children[0].name, - 'Datastore/operation/MongoDB/pipe', - 'should have pipe segment' - ) - t.equal( - 0, - transaction.trace.root.children[0].children.length, - 'pipe should not have any children' - ) - t.end() - }) - - collection.find({}).pipe(destination) - }) - } -}) diff --git a/test/versioned/mongodb/legacy/db.tap.js b/test/versioned/mongodb/legacy/db.tap.js deleted file mode 100644 index e05ded4acc..0000000000 --- a/test/versioned/mongodb/legacy/db.tap.js +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' -const semver = require('semver') -const { dbTest, mongoTest } = require('../db-common') -const params = require('../../../lib/params') -const helper = require('../../../lib/agent_helper') -const { pkgVersion, COLLECTIONS, DB_NAME } = require('../common') - -if (semver.satisfies(pkgVersion, '<3')) { - mongoTest('open', function openTest(t, agent) { - const mongodb = require('mongodb') - const server = new mongodb.Server(params.mongodb_host, params.mongodb_port) - const db = new mongodb.Db(DB_NAME, server) - - helper.runInTransaction(agent, function inTransaction(transaction) { - db.open(function onOpen(err, _db) { - const segment = agent.tracer.getSegment() - t.error(err, 'db.open should not error') - t.equal(db, _db, 'should pass through the arguments correctly') - t.equal(agent.getTransaction(), transaction, 'should not lose tx state') - t.equal(segment.name, 'Callback: onOpen', 'should create segments') - t.equal(transaction.trace.root.children.length, 1, 'should only create one') - const parent = transaction.trace.root.children[0] - t.equal(parent.name, 'Datastore/operation/MongoDB/open', 'should name segment correctly') - t.not(parent.children.indexOf(segment), -1, 'should have callback as child') - db.close() - t.end() - }) - }) - }) - - dbTest('logout', function logoutTest(t, db, verify) { - db.logout({}, function loggedOut(err) { - t.error(err, 'should not have error') - verify(['Datastore/operation/MongoDB/logout', 'Callback: loggedOut'], { legacy: true }) - }) - }) -} - -dbTest('addUser, authenticate, removeUser', function addUserTest(t, db, verify) { - const userName = 'user-test' - const userPass = 'user-test-pass' - - db.removeUser(userName, function preRemove() { - // Don't care if this first remove fails, it's just to ensure a clean slate. - db.addUser(userName, userPass, { roles: ['readWrite'] }, added) - }) - - function added(err) { - if (!t.error(err, 'addUser should not have error')) { - return t.end() - } - - if (typeof db.authenticate === 'function') { - db.authenticate(userName, userPass, authed) - } else { - t.comment('Skipping authentication test, not supported on db') - db.removeUser(userName, removedNoAuth) - } - } - - function authed(err) { - if (!t.error(err, 'authenticate should not have error')) { - return t.end() - } - db.removeUser(userName, removed) - } - - function removed(err) { - if (!t.error(err, 'removeUser should not have error')) { - return t.end() - } - verify( - [ - 'Datastore/operation/MongoDB/removeUser', - 'Callback: preRemove', - 'Datastore/operation/MongoDB/addUser', - 'Callback: added', - 'Datastore/operation/MongoDB/authenticate', - 'Callback: authed', - 'Datastore/operation/MongoDB/removeUser', - 'Callback: removed' - ], - { legacy: true } - ) - } - - function removedNoAuth(err) { - if (!t.error(err, 'removeUser should not have error')) { - return t.end() - } - verify( - [ - 'Datastore/operation/MongoDB/removeUser', - 'Callback: preRemove', - 'Datastore/operation/MongoDB/addUser', - 'Callback: added', - 'Datastore/operation/MongoDB/removeUser', - 'Callback: removedNoAuth' - ], - { legacy: true } - ) - } -}) - -dbTest('collection', function collectionTest(t, db, verify) { - db.collection(COLLECTIONS.collection1, function gotCollection(err, collection) { - t.error(err, 'should not have error') - t.ok(collection, 'collection is not null') - verify(['Datastore/operation/MongoDB/collection', 'Callback: gotCollection'], { legacy: true }) - }) -}) - -dbTest('eval', function evalTest(t, db, verify) { - db.eval('function (x) {return x;}', [3], function evaled(err, result) { - t.error(err, 'should not have error') - t.equal(3, result, 'should produce the right result') - verify(['Datastore/operation/MongoDB/eval', 'Callback: evaled'], { legacy: true }) - }) -}) - -dbTest('collections', function collectionTest(t, db, verify) { - db.collections(function gotCollections(err2, collections) { - t.error(err2, 'should not have error') - t.ok(Array.isArray(collections), 'got array of collections') - verify(['Datastore/operation/MongoDB/collections', 'Callback: gotCollections'], { - legacy: true - }) - }) -}) - -dbTest('command', function commandTest(t, db, verify) { - db.command({ ping: 1 }, function onCommand(err, result) { - t.error(err, 'should not have error') - t.same(result, { ok: 1 }, 'got correct result') - verify(['Datastore/operation/MongoDB/command', 'Callback: onCommand'], { legacy: true }) - }) -}) - -dbTest('createCollection', function createTest(t, db, verify) { - db.createCollection(COLLECTIONS.collection1, function gotCollection(err, collection) { - t.error(err, 'should not have error') - t.equal( - collection.collectionName || collection.s.name, - COLLECTIONS.collection1, - 'new collection should have the right name' - ) - verify(['Datastore/operation/MongoDB/createCollection', 'Callback: gotCollection'], { - legacy: true - }) - }) -}) - -dbTest('createIndex', function createIndexTest(t, db, verify) { - db.createIndex(COLLECTIONS.collection1, 'foo', function createdIndex(err, result) { - t.error(err, 'should not have error') - t.equal(result, 'foo_1', 'should have the right result') - verify(['Datastore/operation/MongoDB/createIndex', 'Callback: createdIndex'], { legacy: true }) - }) -}) - -dbTest('dropCollection', function dropTest(t, db, verify) { - db.createCollection(COLLECTIONS.collection1, function gotCollection(err) { - t.error(err, 'should not have error getting collection') - - db.dropCollection(COLLECTIONS.collection1, function droppedCollection(err, result) { - t.error(err, 'should not have error dropping collection') - t.ok(result === true, 'result should be boolean true') - verify( - [ - 'Datastore/operation/MongoDB/createCollection', - 'Callback: gotCollection', - 'Datastore/operation/MongoDB/dropCollection', - 'Callback: droppedCollection' - ], - { legacy: true } - ) - }) - }) -}) - -dbTest('dropDatabase', function dropDbTest(t, db, verify) { - db.dropDatabase(function droppedDatabase(err, result) { - t.error(err, 'should not have error') - t.ok(result, 'result should be truthy') - verify(['Datastore/operation/MongoDB/dropDatabase', 'Callback: droppedDatabase'], { - legacy: true - }) - }) -}) - -dbTest('ensureIndex', function ensureIndexTest(t, db, verify) { - db.ensureIndex(COLLECTIONS.collection1, 'foo', function ensuredIndex(err, result) { - t.error(err, 'should not have error') - t.equal(result, 'foo_1') - verify(['Datastore/operation/MongoDB/ensureIndex', 'Callback: ensuredIndex'], { legacy: true }) - }) -}) - -dbTest('indexInformation', function indexInfoTest(t, db, verify) { - db.ensureIndex(COLLECTIONS.collection1, 'foo', function ensuredIndex(err) { - t.error(err, 'ensureIndex should not have error') - db.indexInformation(COLLECTIONS.collection1, function gotInfo(err2, result) { - t.error(err2, 'indexInformation should not have error') - t.same(result, { _id_: [['_id', 1]], foo_1: [['foo', 1]] }, 'result is the expected object') - verify( - [ - 'Datastore/operation/MongoDB/ensureIndex', - 'Callback: ensuredIndex', - 'Datastore/operation/MongoDB/indexInformation', - 'Callback: gotInfo' - ], - { legacy: true } - ) - }) - }) -}) - -dbTest('renameCollection', function (t, db, verify) { - db.createCollection(COLLECTIONS.collection1, function gotCollection(err) { - t.error(err, 'should not have error getting collection') - db.renameCollection( - COLLECTIONS.collection1, - COLLECTIONS.collection2, - function renamedCollection(err2) { - t.error(err2, 'should not have error renaming collection') - db.dropCollection(COLLECTIONS.collection2, function droppedCollection(err3) { - t.error(err3) - verify( - [ - 'Datastore/operation/MongoDB/createCollection', - 'Callback: gotCollection', - 'Datastore/operation/MongoDB/renameCollection', - 'Callback: renamedCollection', - 'Datastore/operation/MongoDB/dropCollection', - 'Callback: droppedCollection' - ], - { legacy: true } - ) - }) - } - ) - }) -}) - -dbTest('stats', function statsTest(t, db, verify) { - db.stats({}, function gotStats(err, stats) { - t.error(err, 'should not have error') - t.ok(stats, 'got stats') - verify(['Datastore/operation/MongoDB/stats', 'Callback: gotStats'], { legacy: true }) - }) -}) diff --git a/test/versioned/mongodb/legacy/find.tap.js b/test/versioned/mongodb/legacy/find.tap.js deleted file mode 100644 index fc51d1b81c..0000000000 --- a/test/versioned/mongodb/legacy/find.tap.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' -const common = require('../collection-common') -const { STATEMENT_PREFIX } = require('../common') - -const findOpt = { returnOriginal: false } - -common.test('findAndModify', function findAndModifyTest(t, collection, verify) { - collection.findAndModify({ i: 1 }, [['i', 1]], { $set: { a: 15 } }, { new: true }, done) - - function done(err, data) { - t.error(err) - t.equal(data.value.a, 15) - t.equal(data.value.i, 1) - t.equal(data.ok, 1) - verify(null, [`${STATEMENT_PREFIX}/findAndModify`, 'Callback: done'], ['findAndModify']) - } -}) - -common.test('findAndRemove', function findAndRemoveTest(t, collection, verify) { - collection.findAndRemove({ i: 1 }, [['i', 1]], function done(err, data) { - t.error(err) - t.equal(data.value.i, 1) - t.equal(data.ok, 1) - verify(null, [`${STATEMENT_PREFIX}/findAndRemove`, 'Callback: done'], ['findAndRemove']) - }) -}) - -common.test('findOne', function findOneTest(t, collection, verify) { - collection.findOne({ i: 15 }, function done(err, data) { - t.error(err) - t.equal(data.i, 15) - verify(null, [`${STATEMENT_PREFIX}/findOne`, 'Callback: done'], ['findOne']) - }) -}) - -common.test('findOneAndDelete', function findOneAndDeleteTest(t, collection, verify) { - collection.findOneAndDelete({ i: 15 }, function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - t.equal(data.value.i, 15) - verify(null, [`${STATEMENT_PREFIX}/findOneAndDelete`, 'Callback: done'], ['findOneAndDelete']) - }) -}) - -common.test('findOneAndReplace', function findAndReplaceTest(t, collection, verify) { - collection.findOneAndReplace({ i: 15 }, { b: 15 }, findOpt, done) - - function done(err, data) { - t.error(err) - t.equal(data.value.b, 15) - t.equal(data.ok, 1) - verify(null, [`${STATEMENT_PREFIX}/findOneAndReplace`, 'Callback: done'], ['findOneAndReplace']) - } -}) - -common.test('findOneAndUpdate', function findOneAndUpdateTest(t, collection, verify) { - collection.findOneAndUpdate({ i: 15 }, { $set: { a: 15 } }, findOpt, done) - - function done(err, data) { - t.error(err) - t.equal(data.value.a, 15) - t.equal(data.ok, 1) - verify(null, [`${STATEMENT_PREFIX}/findOneAndUpdate`, 'Callback: done'], ['findOneAndUpdate']) - } -}) diff --git a/test/versioned/mongodb/legacy/index.tap.js b/test/versioned/mongodb/legacy/index.tap.js deleted file mode 100644 index 60f168b78d..0000000000 --- a/test/versioned/mongodb/legacy/index.tap.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const common = require('../collection-common') -const { STATEMENT_PREFIX, DB_NAME, COLLECTIONS } = require('../common') - -common.test('createIndex', function createIndexTest(t, collection, verify) { - collection.createIndex('i', function onIndex(err, data) { - t.error(err) - t.equal(data, 'i_1') - verify(null, [`${STATEMENT_PREFIX}/createIndex`, 'Callback: onIndex'], ['createIndex']) - }) -}) - -common.test('dropIndex', function dropIndexTest(t, collection, verify) { - collection.createIndex('i', function onIndex(err) { - t.error(err) - collection.dropIndex('i_1', function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - verify( - null, - [ - `${STATEMENT_PREFIX}/createIndex`, - 'Callback: onIndex', - `${STATEMENT_PREFIX}/dropIndex`, - 'Callback: done' - ], - ['createIndex', 'dropIndex'] - ) - }) - }) -}) - -common.test('indexes', function indexesTest(t, collection, verify) { - collection.indexes(function done(err, data) { - t.error(err) - const result = data && data[0] - const expectedResult = { - v: result && result.v, - key: { _id: 1 }, - name: '_id_', - ns: `${DB_NAME}.${COLLECTIONS.collection1}` - } - - t.same(result, expectedResult, 'should have expected results') - - verify(null, [`${STATEMENT_PREFIX}/indexes`, 'Callback: done'], ['indexes']) - }) -}) - -common.test('indexExists', function indexExistsTest(t, collection, verify) { - collection.indexExists(['_id_'], function done(err, data) { - t.error(err) - t.equal(data, true) - - verify(null, [`${STATEMENT_PREFIX}/indexExists`, 'Callback: done'], ['indexExists']) - }) -}) - -common.test('indexInformation', function indexInformationTest(t, collection, verify) { - collection.indexInformation(function done(err, data) { - t.error(err) - t.same(data && data._id_, [['_id', 1]], 'should have expected results') - - verify(null, [`${STATEMENT_PREFIX}/indexInformation`, 'Callback: done'], ['indexInformation']) - }) -}) - -common.test('dropAllIndexes', function dropAllIndexesTest(t, collection, verify) { - collection.dropAllIndexes(function done(err, data) { - t.error(err) - t.equal(data, true) - verify(null, [`${STATEMENT_PREFIX}/dropAllIndexes`, 'Callback: done'], ['dropAllIndexes']) - }) -}) - -common.test('ensureIndex', function ensureIndexTest(t, collection, verify) { - collection.ensureIndex('i', function done(err, data) { - t.error(err) - t.equal(data, 'i_1') - verify(null, [`${STATEMENT_PREFIX}/ensureIndex`, 'Callback: done'], ['ensureIndex']) - }) -}) - -common.test('reIndex', function reIndexTest(t, collection, verify) { - collection.reIndex(function done(err, data) { - t.error(err) - t.equal(data, true) - - verify(null, [`${STATEMENT_PREFIX}/reIndex`, 'Callback: done'], ['reIndex']) - }) -}) diff --git a/test/versioned/mongodb/legacy/misc.tap.js b/test/versioned/mongodb/legacy/misc.tap.js deleted file mode 100644 index b1cd35b3c7..0000000000 --- a/test/versioned/mongodb/legacy/misc.tap.js +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const common = require('../collection-common') -const semver = require('semver') -const { pkgVersion, STATEMENT_PREFIX, COLLECTIONS, DB_NAME } = require('../common') - -function verifyAggregateData(t, data) { - t.equal(data.length, 3, 'should have expected amount of results') - t.same(data, [{ value: 5 }, { value: 15 }, { value: 25 }], 'should have expected results') -} - -common.test('aggregate', function aggregateTest(t, collection, verify) { - const cursor = collection.aggregate([ - { $sort: { i: 1 } }, - { $match: { mod10: 5 } }, - { $limit: 3 }, - { $project: { value: '$i', _id: 0 } } - ]) - - cursor.toArray(function onResult(err, data) { - verifyAggregateData(t, data) - verify( - err, - [`${STATEMENT_PREFIX}/aggregate`, `${STATEMENT_PREFIX}/toArray`], - ['aggregate', 'toArray'], - { childrenLength: 2, strict: false } - ) - }) -}) - -common.test('bulkWrite', function bulkWriteTest(t, collection, verify) { - collection.bulkWrite( - [{ deleteMany: { filter: {} } }, { insertOne: { document: { a: 1 } } }], - { ordered: true, w: 1 }, - onWrite - ) - - function onWrite(err, data) { - t.error(err) - t.equal(data.insertedCount, 1) - t.equal(data.deletedCount, 30) - verify(null, [`${STATEMENT_PREFIX}/bulkWrite`, 'Callback: onWrite'], ['bulkWrite']) - } -}) - -common.test('count', function countTest(t, collection, verify) { - collection.count(function onCount(err, data) { - t.error(err) - t.equal(data, 30) - verify(null, [`${STATEMENT_PREFIX}/count`, 'Callback: onCount'], ['count']) - }) -}) - -common.test('distinct', function distinctTest(t, collection, verify) { - collection.distinct('mod10', function done(err, data) { - t.error(err) - t.same(data.sort(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - verify(null, [`${STATEMENT_PREFIX}/distinct`, 'Callback: done'], ['distinct']) - }) -}) - -common.test('drop', function dropTest(t, collection, verify) { - collection.drop(function done(err, data) { - t.error(err) - t.equal(data, true) - verify(null, [`${STATEMENT_PREFIX}/drop`, 'Callback: done'], ['drop']) - }) -}) - -if (semver.satisfies(pkgVersion, '<3')) { - common.test('geoNear', function geoNearTest(t, collection, verify) { - collection.ensureIndex({ loc: '2d' }, { bucketSize: 1 }, indexed) - - function indexed(err) { - t.error(err) - collection.geoNear(20, 20, { maxDistance: 5 }, done) - } - - function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - t.equal(data.results.length, 2) - t.equal(data.results[0].obj.i, 21) - t.equal(data.results[1].obj.i, 17) - t.same(data.results[0].obj.loc, [21, 21]) - t.same(data.results[1].obj.loc, [17, 17]) - t.equal(data.results[0].dis, 1.4142135623730951) - t.equal(data.results[1].dis, 4.242640687119285) - verify( - null, - [ - `${STATEMENT_PREFIX}/ensureIndex`, - 'Callback: indexed', - `${STATEMENT_PREFIX}/geoNear`, - 'Callback: done' - ], - ['ensureIndex', 'geoNear'] - ) - } - }) -} - -common.test('isCapped', function isCappedTest(t, collection, verify) { - collection.isCapped(function done(err, data) { - t.error(err) - t.notOk(data) - - verify(null, [`${STATEMENT_PREFIX}/isCapped`, 'Callback: done'], ['isCapped']) - }) -}) - -common.test('mapReduce', function mapReduceTest(t, collection, verify) { - collection.mapReduce(map, reduce, { out: { inline: 1 } }, done) - - function done(err, data) { - t.error(err) - const expectedData = [ - { _id: 0, value: 30 }, - { _id: 1, value: 33 }, - { _id: 2, value: 36 }, - { _id: 3, value: 39 }, - { _id: 4, value: 42 }, - { _id: 5, value: 45 }, - { _id: 6, value: 48 }, - { _id: 7, value: 51 }, - { _id: 8, value: 54 }, - { _id: 9, value: 57 } - ] - - // data is not sorted depending on speed of - // db calls, sort to compare vs expected collection - data.sort((a, b) => a._id - b._id) - t.same(data, expectedData) - - verify(null, [`${STATEMENT_PREFIX}/mapReduce`, 'Callback: done'], ['mapReduce']) - } - - /* eslint-disable */ - function map(obj) { - emit(this.mod10, this.i) - } - /* eslint-enable */ - - function reduce(key, vals) { - return vals.reduce(function sum(prev, val) { - return prev + val - }, 0) - } -}) - -common.test('options', function optionsTest(t, collection, verify) { - collection.options(function done(err, data) { - t.error(err) - - // Depending on the version of the mongo server this will change. - if (data) { - t.same(data, {}, 'should have expected results') - } else { - t.notOk(data, 'should have expected results') - } - - verify(null, [`${STATEMENT_PREFIX}/options`, 'Callback: done'], ['options']) - }) -}) - -common.test('parallelCollectionScan', function (t, collection, verify) { - collection.parallelCollectionScan({ numCursors: 1 }, function done(err, cursors) { - t.error(err) - - cursors[0].toArray(function toArray(err, items) { - t.error(err) - t.equal(items.length, 30) - - const total = items.reduce(function sum(prev, item) { - return item.i + prev - }, 0) - - t.equal(total, 435) - verify( - null, - [ - `${STATEMENT_PREFIX}/parallelCollectionScan`, - 'Callback: done', - `${STATEMENT_PREFIX}/toArray`, - 'Callback: toArray' - ], - ['parallelCollectionScan', 'toArray'] - ) - }) - }) -}) - -common.test('geoHaystackSearch', function haystackSearchTest(t, collection, verify) { - collection.ensureIndex({ loc: 'geoHaystack', type: 1 }, { bucketSize: 1 }, indexed) - - function indexed(err) { - t.error(err) - collection.geoHaystackSearch(15, 15, { maxDistance: 5, search: {} }, done) - } - - function done(err, data) { - t.error(err) - t.equal(data.ok, 1) - t.equal(data.results.length, 2) - t.equal(data.results[0].i, 13) - t.equal(data.results[1].i, 17) - t.same(data.results[0].loc, [13, 13]) - t.same(data.results[1].loc, [17, 17]) - verify( - null, - [ - `${STATEMENT_PREFIX}/ensureIndex`, - 'Callback: indexed', - `${STATEMENT_PREFIX}/geoHaystackSearch`, - 'Callback: done' - ], - ['ensureIndex', 'geoHaystackSearch'] - ) - } -}) - -common.test('group', function groupTest(t, collection, verify) { - collection.group(['mod10'], {}, { count: 0, total: 0 }, count, done) - - function done(err, data) { - t.error(err) - t.same(data.sort(sort), [ - { mod10: 0, count: 3, total: 30 }, - { mod10: 1, count: 3, total: 33 }, - { mod10: 2, count: 3, total: 36 }, - { mod10: 3, count: 3, total: 39 }, - { mod10: 4, count: 3, total: 42 }, - { mod10: 5, count: 3, total: 45 }, - { mod10: 6, count: 3, total: 48 }, - { mod10: 7, count: 3, total: 51 }, - { mod10: 8, count: 3, total: 54 }, - { mod10: 9, count: 3, total: 57 } - ]) - verify(null, [`${STATEMENT_PREFIX}/group`, 'Callback: done'], ['group']) - } - - function count(obj, prev) { - prev.total += obj.i - prev.count++ - } - - function sort(a, b) { - return a.mod10 - b.mod10 - } -}) - -common.test('rename', function renameTest(t, collection, verify) { - collection.rename(COLLECTIONS.collection2, function done(err) { - t.error(err) - - verify(null, [`${STATEMENT_PREFIX}/rename`, 'Callback: done'], ['rename']) - }) -}) - -common.test('stats', function statsTest(t, collection, verify) { - collection.stats({ i: 5 }, function done(err, data) { - t.error(err) - t.equal(data.ns, `${DB_NAME}.${COLLECTIONS.collection1}`) - t.equal(data.count, 30) - t.equal(data.ok, 1) - - verify(null, [`${STATEMENT_PREFIX}/stats`, 'Callback: done'], ['stats']) - }) -}) diff --git a/test/versioned/mongodb/legacy/update.tap.js b/test/versioned/mongodb/legacy/update.tap.js deleted file mode 100644 index 49fb445805..0000000000 --- a/test/versioned/mongodb/legacy/update.tap.js +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const common = require('../collection-common') -const { STATEMENT_PREFIX } = require('../common') - -/** - * The response from the methods in this file differ between versions - * This helper decides which pieces to assert - * - * @param {Object} params fn params - * @param {Tap.Test} params.t tap instance - * @param {Object} params.data result from callback used to assert - * @param {Number} [params.count] count of results - * @param {Object} params.extraValues extra fields to assert - */ -function assertExpectedResult({ t, data, count, extraValues }) { - const expectedResult = { ok: 1, ...extraValues } - if (count) { - expectedResult.n = count - } - t.same(data.result, expectedResult) -} - -common.test('deleteMany', function deleteManyTest(t, collection, verify) { - collection.deleteMany({ mod10: 5 }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 3 - }) - verify(null, [`${STATEMENT_PREFIX}/deleteMany`, 'Callback: done'], ['deleteMany']) - }) -}) - -common.test('deleteOne', function deleteOneTest(t, collection, verify) { - collection.deleteOne({ mod10: 5 }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1 - }) - verify(null, [`${STATEMENT_PREFIX}/deleteOne`, 'Callback: done'], ['deleteOne']) - }) -}) - -common.test('insert', function insertTest(t, collection, verify) { - collection.insert({ foo: 'bar' }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1 - }) - - verify(null, [`${STATEMENT_PREFIX}/insert`, 'Callback: done'], ['insert']) - }) -}) - -common.test('insertMany', function insertManyTest(t, collection, verify) { - collection.insertMany([{ foo: 'bar' }, { foo: 'bar2' }], function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 2 - }) - - verify(null, [`${STATEMENT_PREFIX}/insertMany`, 'Callback: done'], ['insertMany']) - }) -}) - -common.test('insertOne', function insertOneTest(t, collection, verify) { - collection.insertOne({ foo: 'bar' }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - extraValues: { - n: 1 - } - }) - - verify(null, [`${STATEMENT_PREFIX}/insertOne`, 'Callback: done'], ['insertOne']) - }) -}) - -common.test('remove', function removeTest(t, collection, verify) { - collection.remove({ mod10: 5 }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 3 - }) - - verify(null, [`${STATEMENT_PREFIX}/remove`, 'Callback: done'], ['remove']) - }) -}) - -common.test('replaceOne', function replaceOneTest(t, collection, verify) { - collection.replaceOne({ i: 5 }, { foo: 'bar' }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1, - extraValues: { - nModified: 1 - } - }) - - verify(null, [`${STATEMENT_PREFIX}/replaceOne`, 'Callback: done'], ['replaceOne']) - }) -}) - -common.test('save', function saveTest(t, collection, verify) { - collection.save({ foo: 'bar' }, function done(err, data) { - t.error(err) - t.same(data.result, { ok: 1, n: 1 }) - - verify(null, [`${STATEMENT_PREFIX}/save`, 'Callback: done'], ['save']) - }) -}) - -common.test('update', function updateTest(t, collection, verify) { - collection.update({ i: 5 }, { $set: { foo: 'bar' } }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 1, - extraValues: { - nModified: 1 - } - }) - - verify(null, [`${STATEMENT_PREFIX}/update`, 'Callback: done'], ['update']) - }) -}) - -common.test('updateMany', function updateManyTest(t, collection, verify) { - collection.updateMany({ mod10: 5 }, { $set: { a: 5 } }, function done(err, data) { - t.error(err) - assertExpectedResult({ - t, - data, - count: 3, - extraValues: { - nModified: 3 - } - }) - - verify(null, [`${STATEMENT_PREFIX}/updateMany`, 'Callback: done'], ['updateMany']) - }) -}) - -common.test('updateOne', function updateOneTest(t, collection, verify) { - collection.updateOne({ i: 5 }, { $set: { a: 5 } }, function done(err, data) { - t.notOk(err, 'should not error') - assertExpectedResult({ - t, - data, - count: 1, - extraValues: { - nModified: 1 - } - }) - - verify(null, [`${STATEMENT_PREFIX}/updateOne`, 'Callback: done'], ['updateOne']) - }) -}) diff --git a/test/versioned/mongodb/package.json b/test/versioned/mongodb/package.json index e2767a1ba3..32855f7195 100644 --- a/test/versioned/mongodb/package.json +++ b/test/versioned/mongodb/package.json @@ -5,28 +5,9 @@ "private": true, "tests": [ { + "comment": "Only tests promise based instrumentation. Callback based instrumentation is tested for v4 of mongodb in `test/version/mongodb-esm` folder", "engines": { - "node": ">=16" - }, - "dependencies": { - "mongodb": { - "versions": ">=2.1 < 4.0.0", - "samples": "2" - } - }, - "files": [ - "legacy/bulk.tap.js", - "legacy/cursor.tap.js", - "legacy/db.tap.js", - "legacy/find.tap.js", - "legacy/index.tap.js", - "legacy/misc.tap.js", - "legacy/update.tap.js" - ] - }, - { - "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "mongodb": ">=4.1.4" @@ -42,8 +23,7 @@ ] } ], - "dependencies": {}, "engines": { - "node": ">=16" + "node": ">=18" } } diff --git a/test/versioned/mysql/package.json b/test/versioned/mysql/package.json index 6dac928689..ba5de65852 100644 --- a/test/versioned/mysql/package.json +++ b/test/versioned/mysql/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "mysql": ">=2.2.0", diff --git a/test/versioned/mysql2/package.json b/test/versioned/mysql2/package.json index 62f29d2b98..9522e4e704 100644 --- a/test/versioned/mysql2/package.json +++ b/test/versioned/mysql2/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "mysql2": ">=2.0.0", diff --git a/test/versioned/nestjs/package.json b/test/versioned/nestjs/package.json index 3894fc524e..eaa43edd18 100644 --- a/test/versioned/nestjs/package.json +++ b/test/versioned/nestjs/package.json @@ -6,10 +6,10 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { - "@nestjs/cli": ">=8.0.0" + "@nestjs/cli": ">=9.0.0" }, "files": [ "nest.tap.js" diff --git a/test/versioned/nextjs/.gitignore b/test/versioned/nextjs/.gitignore new file mode 100644 index 0000000000..dfba4152ac --- /dev/null +++ b/test/versioned/nextjs/.gitignore @@ -0,0 +1,2 @@ +app/.next +app-dir/.next diff --git a/test/versioned/nextjs/app-dir.tap.js b/test/versioned/nextjs/app-dir.tap.js new file mode 100644 index 0000000000..eace892515 --- /dev/null +++ b/test/versioned/nextjs/app-dir.tap.js @@ -0,0 +1,94 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const helpers = require('./helpers') +const NEXT_TRANSACTION_PREFIX = 'WebTransaction/WebFrameworkUri/Nextjs/GET/' +const agentHelper = require('../../lib/agent_helper') + +tap.test('Next.js', (t) => { + t.autoend() + let agent + let server + + t.before(async () => { + await helpers.build(__dirname, 'app-dir') + agent = agentHelper.instrumentMockedAgent({ + attributes: { + include: ['request.parameters.*'] + } + }) + + // TODO: would be nice to run a new server per test so there are not chained failures + // but currently has issues. Potentially due to module caching. + server = await helpers.start(__dirname, 'app-dir', '3002') + }) + + t.teardown(async () => { + await server.close() + agentHelper.unloadAgent(agent) + }) + + t.test('should capture query params for static, non-dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/static/standard?first=one&second=two', 3002) + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.first': 'one', + 'request.parameters.second': 'two' + }) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/standard`) + }) + + t.test('should capture query and route params for static, dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/static/dynamic/testing?queryParam=queryValue', 3002) + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.route.value': 'testing', // route [value] param + 'request.parameters.queryParam': 'queryValue' + }) + + t.notOk(agentAttributes['request.parameters.route.queryParam']) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/dynamic/[value]`) + }) + + t.test( + 'should capture query params for server-side rendered, non-dynamic route, page', + async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + const res = await helpers.makeRequest('/person/1?first=one&second=two', 3002) + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match( + agentAttributes, + { + 'request.parameters.first': 'one', + 'request.parameters.second': 'two' + }, + 'should match transaction attributes' + ) + + t.notOk(agentAttributes['request.parameters.route.first']) + t.notOk(agentAttributes['request.parameters.route.second']) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/person/[id]`) + } + ) +}) diff --git a/test/versioned/nextjs/app-dir/app/layout.js b/test/versioned/nextjs/app-dir/app/layout.js new file mode 100644 index 0000000000..8c28c77620 --- /dev/null +++ b/test/versioned/nextjs/app-dir/app/layout.js @@ -0,0 +1,17 @@ + +export default function Layout({ children }) { +return ( + + + +
+

This is my header

+
+
{children}
+
+

This is my footer

+
+ + + ) +} diff --git a/test/versioned/nextjs/app-dir/app/page.js b/test/versioned/nextjs/app-dir/app/page.js new file mode 100644 index 0000000000..b0b86e1a40 --- /dev/null +++ b/test/versioned/nextjs/app-dir/app/page.js @@ -0,0 +1,11 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export default function MyApp() { + return ( +
This is the homepage
+ ) +} + diff --git a/test/versioned/nextjs/app-dir/app/person/[id]/page.js b/test/versioned/nextjs/app-dir/app/person/[id]/page.js new file mode 100644 index 0000000000..8a045a8cfd --- /dev/null +++ b/test/versioned/nextjs/app-dir/app/person/[id]/page.js @@ -0,0 +1,17 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getPerson } from '../../../lib/functions' + +export default async function Person({ params }) { + const user = await getPerson(params.id) + + return ( +
+
{JSON.stringify(user, null, 4)}
+
+ ) +} + diff --git a/test/versioned/nextjs/app-dir/app/static/dynamic/[value]/page.js b/test/versioned/nextjs/app-dir/app/static/dynamic/[value]/page.js new file mode 100644 index 0000000000..868d8d92d0 --- /dev/null +++ b/test/versioned/nextjs/app-dir/app/static/dynamic/[value]/page.js @@ -0,0 +1,33 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import Head from 'next/head' + +export async function getProps(params) { + return { + title: 'This is a statically built dynamic route page.', + value: params.value + } +} + +export async function generateStaticPaths() { + return [ + { value: 'testing' } + ] +} + + +export default async function Standard({ params }) { + const { title, value } = await getProps(params) + return ( + <> + + {title} + +

{title}

+
Value: {value}
+ + ) +} diff --git a/test/versioned/nextjs/app-dir/app/static/standard/page.js b/test/versioned/nextjs/app-dir/app/static/standard/page.js new file mode 100644 index 0000000000..70b027f0b7 --- /dev/null +++ b/test/versioned/nextjs/app-dir/app/static/standard/page.js @@ -0,0 +1,25 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import Head from 'next/head' + +export async function getProps() { + return { + title: 'This is a standard statically built page.' + } +} + + +export default async function Standard() { + const { title } = await getProps() + return ( + <> + + {title} + +

{title}

+ + ) +} diff --git a/test/versioned/nextjs/app-dir/data.js b/test/versioned/nextjs/app-dir/data.js new file mode 100644 index 0000000000..2e1e40316a --- /dev/null +++ b/test/versioned/nextjs/app-dir/data.js @@ -0,0 +1,28 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const data = [ + { + id: 1, + firstName: 'LeBron', + middleName: 'Raymone', + lastName: 'James', + age: 36 + }, + { + id: 2, + firstName: 'Lil', + middleName: 'Nas', + lastName: 'X', + age: 22 + }, + { + id: 3, + firstName: 'Beyoncé', + middleName: 'Giselle', + lastName: 'Knowles-Carter', + age: 40 + } +] diff --git a/test/versioned/nextjs/app-dir/lib/data.js b/test/versioned/nextjs/app-dir/lib/data.js new file mode 100644 index 0000000000..2e1e40316a --- /dev/null +++ b/test/versioned/nextjs/app-dir/lib/data.js @@ -0,0 +1,28 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const data = [ + { + id: 1, + firstName: 'LeBron', + middleName: 'Raymone', + lastName: 'James', + age: 36 + }, + { + id: 2, + firstName: 'Lil', + middleName: 'Nas', + lastName: 'X', + age: 22 + }, + { + id: 3, + firstName: 'Beyoncé', + middleName: 'Giselle', + lastName: 'Knowles-Carter', + age: 40 + } +] diff --git a/test/versioned/nextjs/app-dir/lib/functions.js b/test/versioned/nextjs/app-dir/lib/functions.js new file mode 100644 index 0000000000..836af9a7cc --- /dev/null +++ b/test/versioned/nextjs/app-dir/lib/functions.js @@ -0,0 +1,6 @@ +import { data } from '../data' +export async function getPerson(id) { + const person = data.find((datum) => datum.id.toString() === id) + + return person || `Could not find person with id of ${id}` +} diff --git a/test/versioned/nextjs/app/data.js b/test/versioned/nextjs/app/data.js new file mode 100644 index 0000000000..2e1e40316a --- /dev/null +++ b/test/versioned/nextjs/app/data.js @@ -0,0 +1,28 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const data = [ + { + id: 1, + firstName: 'LeBron', + middleName: 'Raymone', + lastName: 'James', + age: 36 + }, + { + id: 2, + firstName: 'Lil', + middleName: 'Nas', + lastName: 'X', + age: 22 + }, + { + id: 3, + firstName: 'Beyoncé', + middleName: 'Giselle', + lastName: 'Knowles-Carter', + age: 40 + } +] diff --git a/test/versioned/nextjs/app/middleware.js b/test/versioned/nextjs/app/middleware.js new file mode 100644 index 0000000000..b8c52e248f --- /dev/null +++ b/test/versioned/nextjs/app/middleware.js @@ -0,0 +1,46 @@ +'use strict' +const { NextResponse } = require('next/server') + +module.exports.middleware = async function middleware(request) { + if (request.nextUrl.pathname === '/') { + // This logic is only applied to /about + const response = NextResponse.next() + await new Promise((resolve) => { + setTimeout(resolve, 25) + }) + response.headers.set('x-bob', 'another-header') + return response + } + + if (request.nextUrl.pathname === '/api') { + const response = NextResponse.next() + await new Promise((resolve) => { + setTimeout(resolve, 10) + }) + return response + } + + if (request.nextUrl.pathname.startsWith('/api/person')) { + const response = NextResponse.next() + await new Promise((resolve) => { + setTimeout(resolve, 10) + }) + return response + } + + if (request.nextUrl.pathname.startsWith('/person')) { + const response = NextResponse.next() + await new Promise((resolve) => { + setTimeout(resolve, 10) + }) + return response + } + + if (request.nextUrl.pathname.startsWith('/ssr')) { + const response = NextResponse.next() + await new Promise((resolve) => { + setTimeout(resolve, 10) + }) + return response + } +} diff --git a/test/versioned/nextjs/app/pages/_app.js b/test/versioned/nextjs/app/pages/_app.js new file mode 100644 index 0000000000..e3b6411d03 --- /dev/null +++ b/test/versioned/nextjs/app/pages/_app.js @@ -0,0 +1,10 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +function MyApp({ Component, pageProps }) { + return +} + +export default MyApp diff --git a/test/versioned/nextjs/app/pages/api/hello.js b/test/versioned/nextjs/app/pages/api/hello.js new file mode 100644 index 0000000000..64a3d68f91 --- /dev/null +++ b/test/versioned/nextjs/app/pages/api/hello.js @@ -0,0 +1,8 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export default function handler(req, res) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/test/versioned/nextjs/app/pages/api/person/[id].js b/test/versioned/nextjs/app/pages/api/person/[id].js new file mode 100644 index 0000000000..e8534f7781 --- /dev/null +++ b/test/versioned/nextjs/app/pages/api/person/[id].js @@ -0,0 +1,24 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { data } from '../../../data' + +export default function handler(request, response) { + const { method } = request + + if (method === 'GET') { + const { id } = request.query + + const person = data.find((datum) => datum.id.toString() === id) + + if (!person) { + return response.status(400).json('User not found') + } + + return response.status(200).json(person) + } + + return response.status(400).json({ message: 'Invalid method' }) +} diff --git a/test/versioned/nextjs/app/pages/api/person/index.js b/test/versioned/nextjs/app/pages/api/person/index.js new file mode 100644 index 0000000000..397aa7acbf --- /dev/null +++ b/test/versioned/nextjs/app/pages/api/person/index.js @@ -0,0 +1,22 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { data } from '../../../data' + +export default function handler(request, response) { + const { method } = request + + if (method === 'GET') { + return response.status(200).json(data) + } + + if (method === 'POST') { + const { body } = request + data.push({ ...body, id: data.length + 1 }) + return response.status(200).json(data) + } + + return response.status(400).json({ message: 'invalid method' }) +} diff --git a/test/versioned/nextjs/app/pages/index.js b/test/versioned/nextjs/app/pages/index.js new file mode 100644 index 0000000000..5dbd1c0b51 --- /dev/null +++ b/test/versioned/nextjs/app/pages/index.js @@ -0,0 +1,159 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import Link from 'next/link' +import { useReducer, useState } from 'react' + +function reducer(state, action) { + switch (action.type) { + case 'UPDATE_FIRST_NAME': + return { + ...state, + firstName: action.payload.firstName + } + case 'UPDATE_MIDDLE_NAME': + return { + ...state, + middleName: action.payload.middleName + } + case 'UPDATE_LAST_NAME': + return { + ...state, + lastName: action.payload.lastName + } + case 'UPDATE_AGE': + return { + ...state, + age: action.payload.age + } + case 'CLEAR': + return initialState + default: + return state + } +} + +const initialState = { + firstName: '', + middleName: '', + lastName: '', + age: '' +} + +export default function Home() { + const [state, dispatch] = useReducer(reducer, initialState) + const [data, setData] = useState([]) + + const fetchData = async () => { + const response = await fetch('/api/person') + + if (!response.ok) { + throw new Error(`Error: ${response.status}`) + } + const people = await response.json() + return setData(people) + } + + const postData = async () => { + const response = await fetch('/api/person', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(state) + }) + + if (!response.ok) { + throw new Error(`Error: ${response.status}`) + } + + dispatch({ type: 'CLEAR' }) + const people = await response.json() + return setData(people) + } + return ( +
+
+ + + dispatch({ + type: 'UPDATE_FIRST_NAME', + payload: { firstName: e.target.value } + }) + } + /> + + + dispatch({ + type: 'UPDATE_MIDDLE_NAME', + payload: { middleName: e.target.value } + }) + } + /> + + + dispatch({ + type: 'UPDATE_LAST_NAME', + payload: { lastName: e.target.value } + }) + } + /> + + + dispatch({ + type: 'UPDATE_AGE', + payload: { age: e.target.value } + }) + } + /> +
+
+ + +
+
Data:
+ {data ?
{JSON.stringify(data, null, 4)}
: null} + {data.length > 0 ? ( +
+ Click a button to go to individual page +
+ {data.map((person, index) => ( + + {`${person.firstName} ${person.lastName}`} + + ))} +
+
+ ) : null} +
+ ) +} diff --git a/test/versioned/nextjs/app/pages/person/[id].js b/test/versioned/nextjs/app/pages/person/[id].js new file mode 100644 index 0000000000..f79701988e --- /dev/null +++ b/test/versioned/nextjs/app/pages/person/[id].js @@ -0,0 +1,46 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useRouter } from 'next/router' +import * as http from 'http' + +const Person = ({ user }) => { + const router = useRouter() + + return ( +
+ +
{JSON.stringify(user, null, 4)}
+
+ ) +} + +export async function getServerSideProps(context) { + const { id } = context.params + const host = context.req.headers.host + // TODO: Update to use global fetch once agent can properly + // propagate context through it + const data = await new Promise((resolve, reject) => { + http.get(`http://${host}/api/person/${id}`, (res) => { + let body = '' + res.on('data', (data) => (body += data.toString(('utf8')))) + res.on('end', () => { + resolve(body) + }) + }).on('error', reject) + }) + + if (!data) { + return { + notFound: true + } + } + + return { + props: { user: data } + } +} + +export default Person diff --git a/test/versioned/nextjs/app/pages/ssr/dynamic/person/[id].js b/test/versioned/nextjs/app/pages/ssr/dynamic/person/[id].js new file mode 100644 index 0000000000..95ddf3dc8e --- /dev/null +++ b/test/versioned/nextjs/app/pages/ssr/dynamic/person/[id].js @@ -0,0 +1,27 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useRouter } from 'next/router' +import { data } from '../../../../data' + +export async function getServerSideProps(context) { + const { id } = context.params + const user = data.find((person) => person.id.toString() === id) + + return { + props: { user } + } +} + +export default function Person({ user }) { + const router = useRouter() + + return ( +
+ +
{JSON.stringify(user, null, 4)}
+
+ ) +} diff --git a/test/versioned/nextjs/app/pages/ssr/people.js b/test/versioned/nextjs/app/pages/ssr/people.js new file mode 100644 index 0000000000..c02cb9b390 --- /dev/null +++ b/test/versioned/nextjs/app/pages/ssr/people.js @@ -0,0 +1,24 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useRouter } from 'next/router' +import { data } from '../../data' + +export async function getServerSideProps(context) { + return { + props: { users: data } + } +} + +export default function People({ users }) { + const router = useRouter() + + return ( +
+ +
{JSON.stringify(users)}
+
+ ) +} diff --git a/test/versioned/nextjs/app/pages/static/dynamic/[value].js b/test/versioned/nextjs/app/pages/static/dynamic/[value].js new file mode 100644 index 0000000000..e33ed618b7 --- /dev/null +++ b/test/versioned/nextjs/app/pages/static/dynamic/[value].js @@ -0,0 +1,37 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import Head from 'next/head' + +export async function getStaticProps({ params }) { + return { + props: { + title: 'This is a statically built dynamic route page.', + value: params.value + } + } +} + +export async function getStaticPaths() { + return { + paths: [ + { params: { value: 'testing' } } + ], + fallback: false + } +} + + +export default function Standard({ title, value }) { + return ( + <> + + {title} + +

{title}

+
Value: {value}
+ + ) +} diff --git a/test/versioned/nextjs/app/pages/static/standard.js b/test/versioned/nextjs/app/pages/static/standard.js new file mode 100644 index 0000000000..f193e94aa9 --- /dev/null +++ b/test/versioned/nextjs/app/pages/static/standard.js @@ -0,0 +1,26 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import Head from 'next/head' + +export async function getStaticProps() { + return { + props: { + title: 'This is a standard statically built page.' + } + } +} + + +export default function Standard({ title }) { + return ( + <> + + {title} + +

{title}

+ + ) +} diff --git a/test/versioned/nextjs/attributes.tap.js b/test/versioned/nextjs/attributes.tap.js new file mode 100644 index 0000000000..9678abef78 --- /dev/null +++ b/test/versioned/nextjs/attributes.tap.js @@ -0,0 +1,287 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const helpers = require('./helpers') +const nextPkg = require('next/package.json') +const { + isMiddlewareInstrumentationSupported, + getServerSidePropsSegment +} = require('../../../lib/instrumentation/nextjs/utils') +const middlewareSupported = isMiddlewareInstrumentationSupported(nextPkg.version) +const agentHelper = require('../../lib/agent_helper') + +tap.test('Next.js', (t) => { + t.autoend() + let agent + let server + + t.before(async () => { + await helpers.build(__dirname) + agent = agentHelper.instrumentMockedAgent({ + attributes: { + include: ['request.parameters.*'] + } + }) + + // TODO: would be nice to run a new server per test so there are not chained failures + // but currently has issues. Potentially due to module caching. + server = await helpers.start(__dirname) + }) + + t.teardown(async () => { + await server.close() + agentHelper.unloadAgent(agent) + }) + + t.test('should capture query params for static, non-dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/static/standard?first=one&second=two') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.first': 'one', + 'request.parameters.second': 'two' + }) + }) + + t.test('should capture query and route params for static, dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/static/dynamic/testing?queryParam=queryValue') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.route.value': 'testing', // route [value] param + 'request.parameters.queryParam': 'queryValue' + }) + + t.notOk(agentAttributes['request.parameters.route.queryParam']) + }) + + t.test( + 'should capture query params for server-side rendered, non-dynamic route, page', + async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + const res = await helpers.makeRequest('/ssr/people?first=one&second=two') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match( + agentAttributes, + { + 'request.parameters.first': 'one', + 'request.parameters.second': 'two' + }, + 'should match transaction attributes' + ) + + t.notOk(agentAttributes['request.parameters.route.first']) + t.notOk(agentAttributes['request.parameters.route.second']) + + const segmentAttrs = helpers.getSegmentAgentAttributes( + tx, + 'Nodejs/Nextjs/getServerSideProps//ssr/people' + ) + t.match( + segmentAttrs, + { + 'next.page': '/ssr/people' + }, + 'should match segment attributes' + ) + } + ) + + t.test( + 'should capture query and route params for server-side rendered, dynamic route, page', + async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/ssr/dynamic/person/1?queryParam=queryValue') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.route.id': '1', // route [id] param + 'request.parameters.queryParam': 'queryValue' + }) + t.notOk(agentAttributes['request.parameters.route.queryParam']) + const segmentAttrs = helpers.getSegmentAgentAttributes( + tx, + 'Nodejs/Nextjs/getServerSideProps//ssr/dynamic/person/[id]' + ) + t.match( + segmentAttrs, + { + 'next.page': '/ssr/dynamic/person/[id]' + }, + 'should match segment attributes' + ) + } + ) + + t.test('should capture query params for API with non-dynamic route', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + const res = await helpers.makeRequest('/api/hello?first=one&second=two') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.first': 'one', + 'request.parameters.second': 'two' + }) + t.notOk(agentAttributes['request.parameters.route.first']) + t.notOk(agentAttributes['request.parameters.route.second']) + }) + + t.test('should capture query and route params for API with dynamic route', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/api/person/2?queryParam=queryValue') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + const agentAttributes = helpers.getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.route.id': '2', // route [id] param + 'request.parameters.queryParam': 'queryValue' + }) + t.notOk(agentAttributes['request.parameters.route.queryParam']) + }) + + t.test('should have matching traceId, sampled attributes across internal requests', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/person/2') + t.equal(res.statusCode, 200) + + const transactions = await txPromise + t.equal(transactions.length, 2) + + const [transaction1, transaction2] = transactions + + const transaction1Attributes = helpers.getTransactionIntrinsicAttributes(transaction1) + const transaction2Attributes = helpers.getTransactionIntrinsicAttributes(transaction2) + + t.equal(transaction1Attributes.traceId, transaction2Attributes.traceId) + t.equal(transaction1Attributes.sampled, transaction2Attributes.sampled) + }) + ;[true, false].forEach((enabled) => { + t.test( + `should ${enabled ? 'add' : 'not add'} CLM attrs for API with dynamic route`, + async (t) => { + // need to define config like this as agent version could be less than + // when this configuration was defined + agent.config.code_level_metrics = { enabled } + const txPromise = helpers.setupTransactionHandler({ t, agent }) + await helpers.makeRequest('/api/person/2?queryParam=queryValue') + const [tx] = await txPromise + const rootSegment = tx.trace.root + const segments = [ + { + segment: rootSegment.children[0], + name: 'handler', + filepath: 'pages/api/person/[id]' + } + ] + if (middlewareSupported) { + segments.push({ + segment: rootSegment.children[0].children[0], + name: 'middleware', + filepath: 'middleware' + }) + } + t.clmAttrs({ + segments, + enabled, + skipFull: true + }) + } + ) + + t.test(`should ${enabled ? 'add' : 'not add'} CLM attrs to server side page`, async (t) => { + agent.config.code_level_metrics = { enabled } + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + await helpers.makeRequest('/ssr/people') + const [tx] = await txPromise + const rootSegment = tx.trace.root + const segments = [] + if (middlewareSupported) { + segments.push({ + segment: rootSegment.children[0].children[0], + name: 'middleware', + filepath: 'middleware' + }) + segments.push({ + segment: rootSegment.children[0].children[1], + name: 'getServerSideProps', + filepath: 'pages/ssr/people' + }) + } else { + segments.push({ + segment: getServerSidePropsSegment(rootSegment), + name: 'getServerSideProps', + filepath: 'pages/ssr/people' + }) + } + + t.clmAttrs({ + segments, + enabled, + skipFull: true + }) + }) + + t.test('should not add CLM attrs to static page segment', async (t) => { + agent.config.code_level_metrics = { enabled } + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + await helpers.makeRequest('/static/dynamic/testing?queryParam=queryValue') + const [tx] = await txPromise + const rootSegment = tx.trace.root + + // The segment that names the static page will not contain CLM regardless of the + // configuration flag + t.clmAttrs({ + segments: [{ segment: rootSegment.children[0] }], + enabled: false, + skipFull: true + }) + + if (middlewareSupported) { + // this will exist when CLM is enabled + t.clmAttrs({ + segments: [ + { + segment: rootSegment.children[0].children[0], + name: 'middleware', + filepath: 'middleware' + } + ], + enabled, + skipFull: true + }) + } + }) + }) +}) diff --git a/test/versioned/nextjs/helpers.js b/test/versioned/nextjs/helpers.js new file mode 100644 index 0000000000..eef6970d38 --- /dev/null +++ b/test/versioned/nextjs/helpers.js @@ -0,0 +1,168 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const helpers = module.exports +const { exec } = require('child_process') +const http = require('http') +const nextPkg = require('next/package.json') +const semver = require('semver') +const newServerResponse = semver.gte(nextPkg.version, '13.3.0') +const noServerClose = semver.gte(nextPkg.version, '13.4.15') +// In 14.1.0 they removed handling exit event to close server. +// SIGTERM existed for a few past versions but not all the way back to 13.4.15 +// just emit SIGTERM after 14.1.0 +const closeEvent = semver.gte(nextPkg.version, '14.1.0') ? 'SIGTERM' : 'exit' +const { DESTINATIONS } = require('../../../lib/config/attribute-filter') + +/** + * Builds a Next.js app + * @param {sting} dir directory to run next cli in + * @param {string} [path=app] path to app + * @returns {Promise} + * + */ +helpers.build = function build(dir, path = 'app') { + return new Promise((resolve, reject) => { + exec( + `./node_modules/.bin/next build ${path}`, + { + cwd: dir + }, + function cb(err, data) { + if (err) { + reject(err) + } + + resolve(data) + } + ) + }) +} + +/** + * Bootstraps and starts the Next.js app + * @param {sting} dir directory to run next cli in + * @param {string} [path=app] path to app + * @param {number} [port=3001] + * @returns {Promise} + */ +helpers.start = async function start(dir, path = 'app', port = 3001) { + // Needed to support the various locations tests may get loaded from (versioned VS tap VS IDE debugger) + const fullPath = `${dir}/${path}` + + const { startServer } = require(`${dir}/node_modules/next/dist/server/lib/start-server`) + const app = await startServer({ + dir: fullPath, + hostname: '0.0.0.0', + port, + allowRetry: true + }) + + if (noServerClose) { + // 13.4.15 updated startServer to have no return value, so we have to use an event emitter instead for cleanup to fire + // See: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/start-server.ts#L192 + return { close: () => process.emit(closeEvent) } + } + + if (newServerResponse) { + // app is actually a shutdown function, so wrap it for convenience + return { close: app } + } + + await app.prepare() + return app.options.httpServer +} + +/** + * Makes a http GET request to uri specified + * + * @param {string} uri make sure to include `/` + * @param {number} [port=3001] + * @returns {Promise} + */ +helpers.makeRequest = function (uri, port = 3001) { + const url = `http://0.0.0.0:${port}${uri}` + return new Promise((resolve, reject) => { + http + .get(url, (res) => { + resolve(res) + }) + .on('error', reject) + }) +} + +/** + * Registers all instrumentation for Next.js + * + * @param {Agent} agent + */ +helpers.registerInstrumentation = function (agent) { + const hooks = require('../../nr-hooks') + hooks.forEach(agent.registerInstrumentation) +} + +helpers.findSegmentByName = function (root, name) { + if (root.name === name) { + return root + } else if (root.children && root.children.length) { + for (let i = 0; i < root.children.length; i++) { + const child = root.children[i] + const found = helpers.findSegmentByName(child, name) + if (found) { + return found + } + } + } + + return null +} + +helpers.getTransactionEventAgentAttributes = function getTransactionEventAgentAttributes( + transaction +) { + return transaction.trace.attributes.get(DESTINATIONS.TRANS_EVENT) +} + +helpers.getTransactionIntrinsicAttributes = function getTransactionIntrinsicAttributes( + transaction +) { + return transaction.trace.intrinsics +} + +helpers.getSegmentAgentAttributes = function getSegmentAgentAttributes(transaction, name) { + const segment = helpers.findSegmentByName(transaction.trace.root, name) + if (segment) { + return segment.attributes.get(DESTINATIONS.SPAN_EVENT) + } + + return {} +} + +// since we setup agent in before we need to remove +// the transactionFinished listener between tests to avoid +// context leaking +helpers.setupTransactionHandler = function setupTransactionHandler({ + t, + agent, + expectedCount = 1 +}) { + const transactions = [] + return new Promise((resolve) => { + function txHandler(transaction) { + transactions.push(transaction) + if (expectedCount === transactions.length) { + resolve(transactions) + } + } + + agent.on('transactionFinished', txHandler) + + t.teardown(() => { + agent.removeListener('transactionFinished', txHandler) + }) + }) +} diff --git a/test/versioned/nextjs/newrelic.js b/test/versioned/nextjs/newrelic.js new file mode 100644 index 0000000000..8e694d90db --- /dev/null +++ b/test/versioned/nextjs/newrelic.js @@ -0,0 +1,11 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +exports.config = { + app_name: ['My Application'], + license_key: 'license key here' +} diff --git a/test/versioned/nextjs/next.config.js b/test/versioned/nextjs/next.config.js new file mode 100644 index 0000000000..5e0918f9a2 --- /dev/null +++ b/test/versioned/nextjs/next.config.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = { + eslint: { + // Warning: This allows production builds to successfully complete even if + // your project has ESLint errors. + ignoreDuringBuilds: true + }, + experimental: { + appDir: true + } +} diff --git a/test/versioned/nextjs/package.json b/test/versioned/nextjs/package.json new file mode 100644 index 0000000000..e6ac386ea2 --- /dev/null +++ b/test/versioned/nextjs/package.json @@ -0,0 +1,37 @@ +{ + "name": "nextjs-tests", + "targets": [{"name":"next","minAgentVersion":"12.0.0"}], + "version": "0.0.0", + "private": true, + "tests": [ + { + "engines": { + "node": ">=18" + }, + "dependencies": { + "next": ">=13.4.19", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "files": [ + "app-dir.tap.js" + ] + }, + { + "engines": { + "node": ">=18" + }, + "dependencies": { + "next": ">=14.0.0", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "files": [ + "attributes.tap.js", + "segments.tap.js", + "transaction-naming.tap.js" + ] + } + ], + "dependencies": {} +} diff --git a/test/versioned/nextjs/segments.tap.js b/test/versioned/nextjs/segments.tap.js new file mode 100644 index 0000000000..6f224240ae --- /dev/null +++ b/test/versioned/nextjs/segments.tap.js @@ -0,0 +1,127 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const semver = require('semver') +const helpers = require('./helpers') +const TRANSACTION_PREFX = 'WebTransaction/WebFrameworkUri/Nextjs/GET/' +const SEGMENT_PREFIX = 'Nodejs/Nextjs/getServerSideProps/' +const MW_PREFIX = 'Nodejs/Middleware/Nextjs/' +const nextPkg = require('next/package.json') +const { + isMiddlewareInstrumentationSupported +} = require('../../../lib/instrumentation/nextjs/utils') +const agentHelper = require('../../lib/agent_helper') +require('../../lib/metrics_helper') + +function getChildSegments(uri) { + const segments = [ + { + name: `${SEGMENT_PREFIX}${uri}` + } + ] + + if (isMiddlewareInstrumentationSupported(nextPkg.version)) { + segments.unshift({ + name: `${MW_PREFIX}/middleware` + }) + } + + return segments +} + +tap.test('Next.js', (t) => { + t.autoend() + let agent + let server + + t.before(async () => { + agent = agentHelper.instrumentMockedAgent() + // assigning the fake agent to the require cache because in + // app/pages/_document we require the agent and want to not + // try to bootstrap a new, real one + agent.getBrowserTimingHeader = function getBrowserTimingHeader() { + return '
stub
' + } + require.cache.__NR_cache = agent + await helpers.build(__dirname) + server = await helpers.start(__dirname) + }) + + t.teardown(async () => { + await server.close() + agentHelper.unloadAgent(agent) + }) + + t.test('should properly name getServerSideProps segments on static pages', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const URI = '/ssr/people' + + const res = await helpers.makeRequest(URI) + const [tx] = await txPromise + + t.equal(res.statusCode, 200) + const expectedSegments = [ + { + name: `${TRANSACTION_PREFX}${URI}`, + children: getChildSegments(URI) + } + ] + t.assertSegments(tx.trace.root, expectedSegments, { exact: false }) + }) + + t.test('should properly name getServerSideProps segments on dynamic pages', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const EXPECTED_URI = '/ssr/dynamic/person/[id]' + const URI = EXPECTED_URI.replace(/\[id\]/, '1') + + const res = await helpers.makeRequest(URI) + + t.equal(res.statusCode, 200) + const [tx] = await txPromise + const expectedSegments = [ + { + name: `${TRANSACTION_PREFX}${EXPECTED_URI}`, + children: getChildSegments(EXPECTED_URI) + } + ] + t.assertSegments(tx.trace.root, expectedSegments, { exact: false }) + }) + + t.test( + 'should record segment for middleware when making API call', + { skip: !isMiddlewareInstrumentationSupported(nextPkg.version) }, + async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const EXPECTED_URI = '/api/person/[id]' + const URI = EXPECTED_URI.replace(/\[id\]/, '1') + + const res = await helpers.makeRequest(URI) + + t.equal(res.statusCode, 200) + const [tx] = await txPromise + const expectedSegments = [ + { + name: `${TRANSACTION_PREFX}${EXPECTED_URI}` + } + ] + + if (semver.gte(nextPkg.version, '12.2.0')) { + expectedSegments[0].children = [ + { + name: `${MW_PREFIX}/middleware` + } + ] + } + + t.assertSegments(tx.trace.root, expectedSegments, { exact: false }) + } + ) +}) diff --git a/test/versioned/nextjs/transaction-naming.tap.js b/test/versioned/nextjs/transaction-naming.tap.js new file mode 100644 index 0000000000..4b49433400 --- /dev/null +++ b/test/versioned/nextjs/transaction-naming.tap.js @@ -0,0 +1,117 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const helpers = require('./helpers') +const agentHelper = require('../../lib/agent_helper') +const NEXT_TRANSACTION_PREFIX = 'WebTransaction/WebFrameworkUri/Nextjs/GET/' + +tap.test('Next.js', (t) => { + t.autoend() + let agent + let server + + t.before(async () => { + await helpers.build(__dirname) + agent = agentHelper.instrumentMockedAgent({ + attributes: { + include: ['request.parameters.*'] + } + }) + + // TODO: would be nice to run a new server per test so there are not chained failures + // but currently has issues. Potentially due to module caching. + server = await helpers.start(__dirname) + }) + + t.teardown(async () => { + await server.close() + agentHelper.unloadAgent(agent) + }) + + t.test('should properly name static, non-dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + const res = await helpers.makeRequest('/static/standard') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + t.ok(tx) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/standard`) + }) + + t.test('should properly name static, dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + const res = await helpers.makeRequest('/static/dynamic/testing') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + t.ok(tx) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/dynamic/[value]`) + }) + + t.test('should properly name server-side rendered, non-dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/ssr/people') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + t.ok(tx) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/ssr/people`) + }) + + t.test('should properly name server-side rendered, dynamic route, page', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/ssr/dynamic/person/1') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + t.ok(tx) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/ssr/dynamic/person/[id]`) + }) + + t.test('should properly name API with non-dynamic route', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/api/hello') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + t.ok(tx) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/api/hello`) + }) + + t.test('should properly name API with dynamic route', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent }) + + const res = await helpers.makeRequest('/api/person/2') + t.equal(res.statusCode, 200) + const [tx] = await txPromise + + t.ok(tx) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/api/person/[id]`) + }) + + t.test('should properly name transactions with server-side rendered calling API', async (t) => { + const txPromise = helpers.setupTransactionHandler({ t, agent, expectedCount: 2 }) + const res = await helpers.makeRequest('/person/2') + t.equal(res.statusCode, 200) + const transactions = await txPromise + t.equal(transactions.length, 2) + const apiTransaction = transactions.find((transaction) => { + return transaction.name === `${NEXT_TRANSACTION_PREFIX}/api/person/[id]` + }) + + const pageTransaction = transactions.find((transaction) => { + return transaction.name === `${NEXT_TRANSACTION_PREFIX}/person/[id]` + }) + + t.ok(apiTransaction, 'should find transaction matching person API call') + t.ok(pageTransaction, 'should find transaction matching person page call') + }) +}) diff --git a/test/versioned/openai/package.json b/test/versioned/openai/package.json index 199b894d6b..754fc3a6fb 100644 --- a/test/versioned/openai/package.json +++ b/test/versioned/openai/package.json @@ -4,12 +4,12 @@ "version": "0.0.0", "private": true, "engines": { - "node": ">=16" + "node": ">=18" }, "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "openai": ">=4.0.0" diff --git a/test/versioned/pg-esm/package.json b/test/versioned/pg-esm/package.json index 7bcdfdbed6..91ed886d99 100644 --- a/test/versioned/pg-esm/package.json +++ b/test/versioned/pg-esm/package.json @@ -7,7 +7,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { "pg": ">=8.2.0 <8.8.0", @@ -21,7 +21,7 @@ }, { "engines": { - "node": ">=16.12.0" + "node": ">=18" }, "dependencies": { "pg": ">=8.8.0", diff --git a/test/versioned/pg/package.json b/test/versioned/pg/package.json index b311f6573b..0b28021e6d 100644 --- a/test/versioned/pg/package.json +++ b/test/versioned/pg/package.json @@ -6,24 +6,10 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { - "pg": ">=8.2.0 <8.8.0", - "pg-native": ">=2.0.0" - }, - "files": [ - "force-native.tap.js", - "native.tap.js", - "pg.tap.js" - ] - }, - { - "engines": { - "node": ">=16" - }, - "dependencies": { - "pg": ">=8.8.0", + "pg": ">=8.2.0", "pg-native": ">=3.0.0" }, "files": [ diff --git a/test/versioned/pino/package.json b/test/versioned/pino/package.json index 1c292c2ac8..1abccb91a7 100644 --- a/test/versioned/pino/package.json +++ b/test/versioned/pino/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "pino": ">=7.0.0", diff --git a/test/versioned/prisma/package.json b/test/versioned/prisma/package.json index a7cfaf7361..80e2eae762 100644 --- a/test/versioned/prisma/package.json +++ b/test/versioned/prisma/package.json @@ -4,12 +4,12 @@ "version": "0.0.0", "private": true, "engines": { - "node": ">=16" + "node": ">=18" }, "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "@prisma/client": ">=5.0.0 <5.9.0 || >=5.9.1", diff --git a/test/versioned/q/package.json b/test/versioned/q/package.json index fb18389a53..a6fe56e601 100644 --- a/test/versioned/q/package.json +++ b/test/versioned/q/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "q": ">=1.3.0 <2" diff --git a/test/versioned/redis/package.json b/test/versioned/redis/package.json index 72374be169..589c9943fc 100644 --- a/test/versioned/redis/package.json +++ b/test/versioned/redis/package.json @@ -6,10 +6,10 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { - "redis": ">=2.0.0 < 2.3.0" + "redis": ">=3.1.0 < 4.0.0" }, "files": [ "redis.tap.js" @@ -17,40 +17,7 @@ }, { "engines": { - "node": ">=16" - }, - "dependencies": { - "redis": ">=2.3.0 < 2.4.0" - }, - "files": [ - "redis.tap.js" - ] - }, - { - "engines": { - "node": ">=16" - }, - "dependencies": { - "redis": ">=2.4.0 < 2.6.0" - }, - "files": [ - "redis.tap.js" - ] - }, - { - "engines": { - "node": ">=16" - }, - "dependencies": { - "redis": ">=2.6.0 < 4.0.0" - }, - "files": [ - "redis.tap.js" - ] - }, - { - "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "redis": ">=4.0.0" diff --git a/test/versioned/restify/package.json b/test/versioned/restify/package.json index 322fc6acc3..33a50e31fa 100644 --- a/test/versioned/restify/package.json +++ b/test/versioned/restify/package.json @@ -4,49 +4,12 @@ "version": "0.0.0", "private": true, "tests": [ - { - "engines": { - "node": ">=16" - }, - "dependencies": { - "restify": ">=5.0.0 <7", - "express": "4.16", - "restify-errors": "6.1" - }, - "files": [ - "pre-7/capture-params.tap.js", - "pre-7/ignoring.tap.js", - "pre-7/restify.tap.js", - "pre-7/router.tap.js", - "pre-7/rum.tap.js", - "pre-7/transaction-naming.tap.js" - ] - }, - { - "engines": { - "node": ">=16 < 18" - }, - "dependencies": { - "restify": ">=7.0.0", - "express": "4.16", - "restify-errors": "6.1" - }, - "files": [ - "capture-params.tap.js", - "ignoring.tap.js", - "restify.tap.js", - "rum.tap.js", - "router.tap.js", - "transaction-naming.tap.js", - "with-express.tap.js" - ] - }, { "engines": { "node": ">=18" }, "dependencies": { - "restify": ">=10.0.0", + "restify": ">=11.0.0", "express": "4.16", "restify-errors": "6.1" }, diff --git a/test/versioned/restify/pre-7/capture-params.tap.js b/test/versioned/restify/pre-7/capture-params.tap.js deleted file mode 100644 index 0ba875b9db..0000000000 --- a/test/versioned/restify/pre-7/capture-params.tap.js +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const DESTINATIONS = require('../../../../lib/config/attribute-filter').DESTINATIONS -const test = require('tap').test -const helper = require('../../../lib/agent_helper') -const HTTP_ATTS = require('../../../lib/fixtures').httpAttributes - -test('Restify capture params introspection', function (t) { - t.autoend() - - let agent = null - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent({ - allow_all_headers: false, - attributes: { - enabled: true, - include: ['request.parameters.*'] - } - }) - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - }) - - t.test('simple case with no params', function (t) { - const server = require('restify').createServer() - let port = null - - t.teardown(function () { - server.close() - }) - - agent.on('transactionFinished', function (transaction) { - t.ok(transaction.trace, 'transaction has a trace.') - // on older versions of node response messages aren't included - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - HTTP_ATTS.forEach(function (key) { - t.ok(attributes[key], 'Trace contains expected HTTP attribute: ' + key) - }) - if (attributes.httpResponseMessage) { - t.equal(attributes.httpResponseMessage, 'OK', 'Trace contains httpResponseMessage') - } - }) - - server.get('/test', function (req, res, next) { - t.ok(agent.getTransaction(), 'transaction is available') - - res.send({ status: 'ok' }) - next() - }) - - server.listen(0, function () { - port = server.address().port - helper.makeGetRequest('http://localhost:' + port + '/test', function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected respose') - t.end() - }) - }) - }) - - t.test('case with route params', function (t) { - const server = require('restify').createServer() - let port = null - - t.teardown(function () { - server.close() - }) - - agent.on('transactionFinished', function (transaction) { - t.ok(transaction.trace, 'transaction has a trace.') - // on older versions of node response messages aren't included - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal( - attributes['request.parameters.route.id'], - '1337', - 'Trace attributes include `id` route param' - ) - }) - - server.get('/test/:id', function (req, res, next) { - t.ok(agent.getTransaction(), 'transaction is available') - - res.send({ status: 'ok' }) - next() - }) - - server.listen(0, function () { - port = server.address().port - helper.makeGetRequest('http://localhost:' + port + '/test/1337', function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected respose') - t.end() - }) - }) - }) - - t.test('case with query params', function (t) { - const server = require('restify').createServer() - let port = null - - t.teardown(function () { - server.close() - }) - - agent.on('transactionFinished', function (transaction) { - t.ok(transaction.trace, 'transaction has a trace.') - // on older versions of node response messages aren't included - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal( - attributes['request.parameters.name'], - 'restify', - 'Trace attributes include `name` query param' - ) - }) - - server.get('/test', function (req, res, next) { - t.ok(agent.getTransaction(), 'transaction is available') - - res.send({ status: 'ok' }) - next() - }) - - server.listen(0, function () { - port = server.address().port - const url = 'http://localhost:' + port + '/test?name=restify' - helper.makeGetRequest(url, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected respose') - t.end() - }) - }) - }) - - t.test('case with both route and query params', function (t) { - const server = require('restify').createServer() - let port = null - - t.teardown(function () { - server.close() - }) - - agent.on('transactionFinished', function (transaction) { - t.ok(transaction.trace, 'transaction has a trace.') - // on older versions of node response messages aren't included - const attributes = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) - t.equal( - attributes['request.parameters.route.id'], - '1337', - 'Trace attributes include `id` route param' - ) - t.equal( - attributes['request.parameters.name'], - 'restify', - 'Trace attributes include `name` query param' - ) - }) - - server.get('/test/:id', function (req, res, next) { - t.ok(agent.getTransaction(), 'transaction is available') - - res.send({ status: 'ok' }) - next() - }) - - server.listen(0, function () { - port = server.address().port - const url = 'http://localhost:' + port + '/test/1337?name=restify' - helper.makeGetRequest(url, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected respose') - t.end() - }) - }) - }) -}) diff --git a/test/versioned/restify/pre-7/ignoring.tap.js b/test/versioned/restify/pre-7/ignoring.tap.js deleted file mode 100644 index 6d383e5438..0000000000 --- a/test/versioned/restify/pre-7/ignoring.tap.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const test = require('tap').test -const helper = require('../../../lib/agent_helper') -const API = require('../../../../api') - -test('Restify router introspection', function (t) { - t.plan(7) - - const agent = helper.instrumentMockedAgent() - const api = new API(agent) - const server = require('restify').createServer() - - t.teardown(function () { - server.close(function () { - helper.unloadAgent(agent) - }) - }) - - agent.on('transactionFinished', function (transaction) { - t.equal( - transaction.name, - 'WebTransaction/Restify/GET//polling/:id', - 'transaction has expected name even on error' - ) - - t.ok(transaction.ignore, 'transaction is ignored') - - t.notOk(agent.traces.trace, 'should have no transaction trace') - - const metrics = agent.metrics._metrics.unscoped - // loading k2 adds instrumentation metrics for things it registers - // this also differs between major versions of restify. 6+ also loads - // k2 child_process instrumentation, fun fun fun - const expectedMetrics = helper.isSecurityAgentEnabled(agent) ? 15 : 7 - t.equal( - Object.keys(metrics).length, - expectedMetrics, - 'only supportability metrics added to agent collection' - ) - - const errors = agent.errors.traceAggregator.errors - t.equal(errors.length, 0, 'no errors noticed') - }) - - server.get('/polling/:id', function (req, res, next) { - api.addIgnoringRule(/poll/) - res.send(400, { status: 'pollpollpoll' }) - next() - }) - - server.listen(0, function () { - const port = server.address().port - const url = 'http://localhost:' + port + '/polling/31337' - helper.makeGetRequest(url, function (error, res, body) { - t.equal(res.statusCode, 400, 'got expected error') - t.same(body, { status: 'pollpollpoll' }, 'got expected response') - }) - }) -}) diff --git a/test/versioned/restify/pre-7/newrelic.js b/test/versioned/restify/pre-7/newrelic.js deleted file mode 100644 index 5bfe53711f..0000000000 --- a/test/versioned/restify/pre-7/newrelic.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -exports.config = { - app_name: ['My Application'], - license_key: 'license key here', - logging: { - level: 'trace', - filepath: '../../../newrelic_agent.log' - }, - utilization: { - detect_aws: false, - detect_pcf: false, - detect_azure: false, - detect_gcp: false, - detect_docker: false - }, - transaction_tracer: { - enabled: true - } -} diff --git a/test/versioned/restify/pre-7/restify.tap.js b/test/versioned/restify/pre-7/restify.tap.js deleted file mode 100644 index 607786210a..0000000000 --- a/test/versioned/restify/pre-7/restify.tap.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../../lib/agent_helper') -require('../../../lib/metrics_helper') - -const METRIC = 'WebTransaction/Restify/GET//hello/:name' - -tap.test('Restify', (t) => { - t.autoend() - - let agent = null - let restify = null - t.beforeEach(() => { - agent = helper.instrumentMockedAgent() - - restify = require('restify') - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - t.test('should not crash when handling a connection', function (t) { - t.plan(7) - - const server = restify.createServer() - t.teardown(() => server.close()) - - server.get('/hello/:name', function sayHello(req, res) { - t.ok(agent.getTransaction(), 'transaction should be available in handler') - res.send('hello ' + req.params.name) - }) - - server.listen(0, function () { - const port = server.address().port - t.notOk(agent.getTransaction(), 'transaction should not leak into server') - - const url = `http://localhost:${port}/hello/friend` - helper.makeGetRequest(url, function (error, response, body) { - if (error) { - return t.fail(error) - } - t.notOk(agent.getTransaction(), 'transaction should not leak into external request') - - const metric = agent.metrics.getMetric(METRIC) - t.ok(metric, 'request metrics should have been gathered') - t.equal(metric.callCount, 1, 'handler should have been called') - t.equal(body, 'hello friend', 'should return expected data') - - const isFramework = agent.environment.get('Framework').indexOf('Restify') > -1 - t.ok(isFramework, 'should indicate that restify is a framework') - }) - }) - }) - - t.test('should still be instrumented when run with SSL', function (t) { - t.plan(7) - - helper - .withSSL() - .then(([key, certificate, ca]) => { - const server = restify.createServer({ key: key, certificate: certificate }) - t.teardown(() => server.close()) - - server.get('/hello/:name', function sayHello(req, res) { - t.ok(agent.getTransaction(), 'transaction should be available in handler') - res.send('hello ' + req.params.name) - }) - - server.listen(0, function () { - const port = server.address().port - t.notOk(agent.getTransaction(), 'transaction should not leak into server') - - const url = `https://${helper.SSL_HOST}:${port}/hello/friend` - helper.makeGetRequest(url, { ca }, function (error, response, body) { - if (error) { - t.fail(error) - return t.end() - } - - t.notOk(agent.getTransaction(), 'transaction should not leak into external request') - - const metric = agent.metrics.getMetric(METRIC) - t.ok(metric, 'request metrics should have been gathered') - t.equal(metric.callCount, 1, 'handler should have been called') - t.equal(body, 'hello friend', 'should return expected data') - - const isFramework = agent.environment.get('Framework').indexOf('Restify') > -1 - t.ok(isFramework, 'should indicate that restify is a framework') - }) - }) - }) - .catch((error) => { - t.fail('unable to set up SSL: ' + error) - t.end() - }) - }) - - t.test('should generate middleware metrics', (t) => { - // Metrics for this transaction with the right name. - const expectedMiddlewareMetrics = [ - [{ name: 'WebTransaction/Restify/GET//foo/:bar' }], - [{ name: 'WebTransactionTotalTime/Restify/GET//foo/:bar' }], - [{ name: 'Apdex/Restify/GET//foo/:bar' }], - - // Unscoped middleware metrics. - [{ name: 'Nodejs/Middleware/Restify/middleware//' }], - [{ name: 'Nodejs/Middleware/Restify/middleware2//' }], - [{ name: 'Nodejs/Middleware/Restify/handler//foo/:bar' }], - - // Scoped middleware metrics. - [ - { - name: 'Nodejs/Middleware/Restify/middleware//', - scope: 'WebTransaction/Restify/GET//foo/:bar' - } - ], - [ - { - name: 'Nodejs/Middleware/Restify/middleware2//', - scope: 'WebTransaction/Restify/GET//foo/:bar' - } - ], - [ - { - name: 'Nodejs/Middleware/Restify/handler//foo/:bar', - scope: 'WebTransaction/Restify/GET//foo/:bar' - } - ] - ] - - const server = restify.createServer() - t.teardown(() => server.close()) - - server.use(function middleware(req, res, next) { - t.ok(agent.getTransaction(), 'should be in transaction context') - next() - }) - - server.use(function middleware2(req, res, next) { - t.ok(agent.getTransaction(), 'should be in transaction context') - next() - }) - - server.get('/foo/:bar', function handler(req, res, next) { - t.ok(agent.getTransaction(), 'should be in transaction context') - res.send({ message: 'done' }) - next() - }) - - server.listen(0, function () { - const port = server.address().port - const url = `http://localhost:${port}/foo/bar` - - helper.makeGetRequest(url, function (error) { - t.error(error) - - t.assertMetrics(agent.metrics, expectedMiddlewareMetrics, false, false) - t.end() - }) - }) - }) -}) diff --git a/test/versioned/restify/pre-7/router.tap.js b/test/versioned/restify/pre-7/router.tap.js deleted file mode 100644 index 2fef181b31..0000000000 --- a/test/versioned/restify/pre-7/router.tap.js +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') - -const helper = require('../../../lib/agent_helper') - -tap.test('Restify router', function (t) { - t.autoend() - - let agent = null - let server = null - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent({ - attributes: { - enabled: true, - include: ['request.parameters.*'] - } - }) - - server = require('restify').createServer() - }) - - t.afterEach(function () { - return new Promise((resolve) => { - server.close(function () { - helper.unloadAgent(agent) - resolve() - }) - }) - }) - - t.test('introspection', function (t) { - t.plan(12) - - // need to capture attributes - agent.config.attributes.enabled = true - - agent.on('transactionFinished', function (transaction) { - t.equal( - transaction.name, - 'WebTransaction/Restify/GET//test/:id', - 'transaction has expected name' - ) - t.equal(transaction.url, '/test/31337', 'URL is left alone') - t.equal(transaction.statusCode, 200, 'status code is OK') - t.equal(transaction.verb, 'GET', 'HTTP method is GET') - t.ok(transaction.trace, 'transaction has trace') - - const web = transaction.trace.root.children[0] - t.ok(web, 'trace has web segment') - t.equal(web.name, transaction.name, 'segment name and transaction name match') - t.equal(web.partialName, 'Restify/GET//test/:id', 'should have partial name for apdex') - t.equal( - web.getAttributes()['request.parameters.route.id'], - '31337', - 'namer gets parameters out of route' - ) - }) - - server.get('/test/:id', function (req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available') - - res.send({ status: 'ok' }) - next() - }) - - _listenAndRequest(t, '/test/31337') - }) - - t.test('next(true): continue processing', function (t) { - t.plan(6) - - server.get( - '/test/:id', - function first(req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available') - next(true) - }, - function second(req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available') - next(true) - }, - function final(req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available') - res.send({ status: 'ok' }) - next() - } - ) - - agent.on('transactionFinished', function (tx) { - t.equal(tx.name, 'WebTransaction/Restify/GET//test/:id', 'should have correct name') - }) - - _listenAndRequest(t, '/test/foobar') - }) - - t.test('next(false): stop processing', function (t) { - t.plan(4) - - server.get( - '/test/:id', - function first(req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available') - res.send({ status: 'ok' }) - next(false) - }, - function final(req, res, next) { - t.fail('should not enter this final middleware') - res.send({ status: 'ok' }) - next() - } - ) - - agent.on('transactionFinished', function (tx) { - t.equal(tx.name, 'WebTransaction/Restify/GET//test/:id', 'should have correct name') - }) - - _listenAndRequest(t, '/test/foobar') - }) - - t.test('next("other_route"): jump processing', function (t) { - t.plan(5) - - server.get({ name: 'first', path: '/test/:id' }, function final(req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available') - next('second') - }) - - server.get({ name: 'second', path: '/other' }, function final(req, res, next) { - t.ok(agent.getTransaction(), 'transaction should be available') - res.send({ status: 'ok' }) - next() - }) - - agent.on('transactionFinished', function (tx) { - t.equal(tx.name, 'WebTransaction/Restify/GET//other', 'should have correct name') - }) - - _listenAndRequest(t, '/test/foobar') - }) - - function _listenAndRequest(t, route) { - server.listen(0, function () { - const port = server.address().port - const url = 'http://localhost:' + port + route - helper.makeGetRequest(url, function (error, res, body) { - t.equal(res.statusCode, 200, 'nothing exploded') - t.same(body, { status: 'ok' }, 'got expected respose') - }) - }) - } -}) diff --git a/test/versioned/restify/pre-7/rum.tap.js b/test/versioned/restify/pre-7/rum.tap.js deleted file mode 100644 index 0df9dcfe69..0000000000 --- a/test/versioned/restify/pre-7/rum.tap.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') - -const helper = require('../../../lib/agent_helper') -const API = require('../../../../api') - -tap.test('Restify router introspection', function (t) { - t.plan(3) - - const agent = helper.instrumentMockedAgent() - const server = require('restify').createServer() - const api = new API(agent) - - agent.config.application_id = '12345' - agent.config.browser_monitoring.browser_key = '12345' - agent.config.browser_monitoring.js_agent_loader = 'function(){}' - - t.teardown(() => { - server.close(() => { - helper.unloadAgent(agent) - }) - }) - - server.get('/test/:id', function (req, res, next) { - const rum = api.getBrowserTimingHeader() - t.equal(rum.substring(0, 7), ' { - t.autoend() - - let agent = null - let restify = null - let restifyPkg = null - let server = null - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent() - - restify = require('restify') - restifyPkg = require('restify/package.json') - server = restify.createServer() - }) - - t.afterEach(() => { - return new Promise((resolve) => { - helper.unloadAgent(agent) - if (server) { - server.close(resolve) - } else { - resolve() - } - }) - }) - - t.test('transaction name with single route', (t) => { - t.plan(1) - - server.get('/path1', (req, res, next) => { - res.send() - next() - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('transaction name with async response middleware', (t) => { - t.plan(1) - - // restify v5 added the plugins object - if (restify.plugins && restify.plugins.gzipResponse) { - server.use(restify.plugins.gzipResponse()) - } else { - server.use(restify.gzipResponse()) - } - - server.get('/path1', (req, res, next) => { - res.send({ - patientId: 5, - entries: ['hi', 'bye', 'example'], - total: 3 - }) - next() - }) - - runTest({ - t, - endpoint: '/path1', - expectedName: 'GET//path1', - requestOpts: { headers: { 'Accept-Encoding': 'gzip' } } - }) - }) - - t.test('transaction name with async response middleware (res.json)', (t) => { - t.plan(1) - - // restify v5 added the plugins object - if (restify.plugins && restify.plugins.gzipResponse) { - server.use(restify.plugins.gzipResponse()) - } else { - server.use(restify.gzipResponse()) - } - - server.get('/path1', (req, res, next) => { - res.json({ - patientId: 5, - entries: ['hi', 'bye', 'example'], - total: 3 - }) - next() - }) - - runTest({ - t, - endpoint: '/path1', - expectedName: 'GET//path1', - requestOpts: { headers: { 'Accept-Encoding': 'gzip' } } - }) - }) - - if (semver.satisfies(restifyPkg.version, '>=5')) { - t.test('transaction name with async response middleware (res.sendRaw)', (t) => { - t.plan(1) - - // restify v5 added the plugins object - if (restify.plugins && restify.plugins.gzipResponse) { - server.use(restify.plugins.gzipResponse()) - } else { - server.use(restify.gzipResponse()) - } - - server.get('/path1', (req, res, next) => { - res.sendRaw( - JSON.stringify({ - patientId: 5, - entries: ['hi', 'bye', 'example'], - total: 3 - }) - ) - next() - }) - - runTest({ - t, - endpoint: '/path1', - expectedName: 'GET//path1', - requestOpts: { headers: { 'Accept-Encoding': 'gzip' } } - }) - }) - } - - t.test('transaction name with async response middleware (res.redirect)', (t) => { - t.plan(1) - - // restify v5 added the plugins object - if (restify.plugins && restify.plugins.gzipResponse) { - server.use(restify.plugins.gzipResponse()) - } else { - server.use(restify.gzipResponse()) - } - - server.get('/path1', (req, res, next) => { - res.redirect('http://google.com', next) - }) - - runTest({ - t, - endpoint: '/path1', - expectedName: 'GET//path1', - requestOpts: { headers: { 'Accept-Encoding': 'gzip' } } - }) - }) - - t.test('transaction name with no matched routes', (t) => { - t.plan(1) - - server.get('/path1', (req, res, next) => { - t.fail('should not enter different endpoint') - res.send() - next() - }) - - runTest({ t, endpoint: '/foobar', prefix: 'Nodejs', expectedName: 'GET/(not found)' }) - }) - - t.test('transaction name with route that has multiple handlers', (t) => { - t.plan(3) - - server.get( - '/path1', - (req, res, next) => { - t.pass('should enter first middleware') - next() - }, - (req, res, next) => { - t.pass('should enter second middleware') - res.send() - next() - } - ) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('transaction name with middleware', (t) => { - t.plan(3) - - server.use((req, res, next) => { - t.pass('should enter `use` middleware') - next() - }) - server.get('/path1', (req, res, next) => { - t.pass('should enter route handler') - res.send() - next() - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('multiple route handlers with the same name do not duplicate', (t) => { - t.plan(3) - - server.get({ name: 'first', path: '/path1' }, (req, res, next) => { - t.pass('should execute first handler') - next('second') - }) - - server.get({ name: 'second', path: '/path1' }, (req, res, next) => { - t.pass('should execute second handler') - res.send() - next() - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('responding from middleware', (t) => { - t.plan(2) - - server.use((req, res, next) => { - res.send() - next() - }) - - server.get('/path1', (req, res, next) => { - t.pass('should enter route middleware') - next() - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//' }) - }) - - t.test('with error', (t) => { - t.plan(1) - - const errors = require('restify-errors') - - server.get('/path1', (req, res, next) => { - next(new errors.InternalServerError('foobar')) - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('with error while out of context', (t) => { - t.plan(1) - - const errors = require('restify-errors') - - server.get('/path1', (req, res, next) => { - helper.runOutOfContext(() => { - next(new errors.InternalServerError('foobar')) - }) - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('when using a route variable', (t) => { - t.plan(2) - - server.get('/foo/:bar', (req, res, next) => { - t.equal(req.params.bar, 'fizz', 'should pass through params') - res.send() - next() - }) - - runTest({ t, endpoint: '/foo/fizz', expectedName: 'GET//foo/:bar' }) - }) - - t.test('when using a regular expression in path', (t) => { - t.plan(2) - - server.get(/^\/foo\/(.*)/, (req, res, next) => { - t.equal(req.params[0], 'bar', 'should pass through captured param') - res.send() - next() - }) - - runTest({ t, endpoint: '/foo/bar', expectedName: 'GET//^\\/foo\\/(.*)/' }) - }) - - t.test('when next is called after transaction state loss', (t) => { - t.plan(5) - - server.use((req, res, next) => { - t.ok(agent.getTransaction(), 'should have transaction at start') - req.testTx = agent.getTransaction() - - helper.runOutOfContext(() => { - t.notOk(agent.getTransaction(), 'should lose transaction before next') - next() - }) - }) - - server.get('/path1', (req, res, next) => { - const tx = agent.getTransaction() - t.ok(tx, 'should re-instate transaction in next middleware') - t.equal(tx && tx.id, req.testTx.id, 'should reinstate correct transaction') - res.send() - next() - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('responding after transaction state loss', (t) => { - t.plan(2) - - server.get('/path1', (req, res, next) => { - helper.runOutOfContext(() => { - t.notOk(agent.getTransaction(), 'should have no transaction') - res.send() - next() - }) - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('responding with just a status code', (t) => { - t.plan(1) - - server.get('/path1', (req, res, next) => { - res.send(299) - next() - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - t.test('responding with just a status code after state loss', (t) => { - t.plan(1) - - server.get('/path1', (req, res, next) => { - helper.runOutOfContext(() => { - res.send(299) - next() - }) - }) - - runTest({ t, endpoint: '/path1', expectedName: 'GET//path1' }) - }) - - /** - * @param {Object} cfg - * @property {Object} cfg.t - * @property {string} cfg.endpoint - * @property {string} [cfg.prefix='Restify'] - * @property {string} cfg.expectedName - * @property {Function} [cfg.cb=t.end] - * @property {Object} [cfg.requestOpts=null] - */ - function runTest(cfg) { - const t = cfg.t - const endpoint = cfg.endpoint - const prefix = cfg.prefix || 'Restify' - const expectedName = `WebTransaction/${prefix}/${cfg.expectedName}` - - agent.on('transactionFinished', (tx) => { - t.equal(tx.name, expectedName, 'should have correct name') - ;(cfg.cb && cfg.cb()) || t.end() - }) - - server.listen(() => { - const port = server.address().port - helper.makeGetRequest(`http://localhost:${port}${endpoint}`, cfg.requestOpts || null) - }) - } -}) diff --git a/test/versioned/superagent/package.json b/test/versioned/superagent/package.json index fd1f96ee68..7645b61d0e 100644 --- a/test/versioned/superagent/package.json +++ b/test/versioned/superagent/package.json @@ -5,11 +5,11 @@ "private": true, "tests": [{ "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "superagent": { - "versions": ">=2 <7.1.0 || >=7.1.1", + "versions": ">=3 <7.1.0 || >=7.1.1", "samples": 5 } }, diff --git a/test/versioned/undici/package.json b/test/versioned/undici/package.json index b793ebfdc2..d4bcc523ed 100644 --- a/test/versioned/undici/package.json +++ b/test/versioned/undici/package.json @@ -4,23 +4,12 @@ "version": "0.0.0", "private": true, "tests": [ - { - "engines": { - "node": "16" - }, - "dependencies": { - "undici": ">=4.7.0 <6.0.0" - }, - "files": [ - "requests.tap.js" - ] - }, { "engines": { "node": ">=18" }, "dependencies": { - "undici": ">=4.7.0" + "undici": ">=5.0.0" }, "files": [ "requests.tap.js" @@ -28,6 +17,6 @@ } ], "engines": { - "node": ">=16" + "node": ">=18" } } diff --git a/test/integration/instrumentation/promises/legacy-promise-segments.js b/test/versioned/when/legacy-promise-segments.js similarity index 99% rename from test/integration/instrumentation/promises/legacy-promise-segments.js rename to test/versioned/when/legacy-promise-segments.js index c80285a0c2..3edc160bc6 100644 --- a/test/integration/instrumentation/promises/legacy-promise-segments.js +++ b/test/versioned/when/legacy-promise-segments.js @@ -5,8 +5,8 @@ 'use strict' -const helper = require('../../../lib/agent_helper') -require('../../../lib/metrics_helper') +const helper = require('../../lib/agent_helper') +require('../../lib/metrics_helper') module.exports = runTests diff --git a/test/versioned/when/package.json b/test/versioned/when/package.json index c6c78bd43e..4cdaa7b838 100644 --- a/test/versioned/when/package.json +++ b/test/versioned/when/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "when": ">=3.7.0" diff --git a/test/versioned/when/when.tap.js b/test/versioned/when/when.tap.js index a243731ac8..0d2bfb2c1a 100644 --- a/test/versioned/when/when.tap.js +++ b/test/versioned/when/when.tap.js @@ -6,9 +6,8 @@ 'use strict' const helper = require('../../lib/agent_helper') -const TEST_DIR = '../../integration/instrumentation/promises/' -const testPromiseSegments = require(`${TEST_DIR}/legacy-promise-segments`) -const testTransactionState = require(`${TEST_DIR}/transaction-state`) +const testPromiseSegments = require(`./legacy-promise-segments`) +const testTransactionState = require(`../../lib/promises/transaction-state`) // grab process emit before tap / async-hooks-domain can mess with it const originalEmit = process.emit diff --git a/test/versioned/winston-esm/package.json b/test/versioned/winston-esm/package.json index 79d1eb36e8..c156c8b537 100644 --- a/test/versioned/winston-esm/package.json +++ b/test/versioned/winston-esm/package.json @@ -7,7 +7,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "winston": ">=3.0.0", diff --git a/test/versioned/winston/package.json b/test/versioned/winston/package.json index 8d7aca9aef..ece74c3f67 100644 --- a/test/versioned/winston/package.json +++ b/test/versioned/winston/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16" + "node": ">=18" }, "dependencies": { "winston": ">=3.0.0" diff --git a/third_party_manifest.json b/third_party_manifest.json index e95ef83fa2..0ce59d6bd6 100644 --- a/third_party_manifest.json +++ b/third_party_manifest.json @@ -1,5 +1,5 @@ { - "lastUpdated": "Thu Jun 06 2024 16:54:51 GMT-0400 (Eastern Daylight Time)", + "lastUpdated": "Wed Jul 31 2024 12:48:28 GMT-0400 (Eastern Daylight Time)", "projectName": "New Relic Node Agent", "projectUrl": "https://github.com/newrelic/node-newrelic", "includeOptDeps": true, @@ -16,15 +16,15 @@ "licenseTextSource": "file", "publisher": "Contrast Security" }, - "@newrelic/native-metrics@10.1.1": { + "@newrelic/native-metrics@10.2.0": { "name": "@newrelic/native-metrics", - "version": "10.1.1", + "version": "10.2.0", "range": "^10.0.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/newrelic/node-native-metrics", - "versionedRepoUrl": "https://github.com/newrelic/node-native-metrics/tree/v10.1.1", + "versionedRepoUrl": "https://github.com/newrelic/node-native-metrics/tree/v10.2.0", "licenseFile": "node_modules/@newrelic/native-metrics/LICENSE", - "licenseUrl": "https://github.com/newrelic/node-native-metrics/blob/v10.1.1/LICENSE", + "licenseUrl": "https://github.com/newrelic/node-native-metrics/blob/v10.2.0/LICENSE", "licenseTextSource": "file", "publisher": "New Relic Node.js agent team", "email": "nodejs@newrelic.com" @@ -44,15 +44,15 @@ }, "includeDev": true, "dependencies": { - "@grpc/grpc-js@1.10.8": { + "@grpc/grpc-js@1.11.1": { "name": "@grpc/grpc-js", - "version": "1.10.8", + "version": "1.11.1", "range": "^1.9.4", "licenses": "Apache-2.0", "repoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", - "versionedRepoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.10.8", + "versionedRepoUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.11.1", "licenseFile": "node_modules/@grpc/grpc-js/LICENSE", - "licenseUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.10.8/LICENSE", + "licenseUrl": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.11.1/LICENSE", "licenseTextSource": "file", "publisher": "Google Inc." }, @@ -82,15 +82,15 @@ "email": "w@tson.dk", "url": "https://twitter.com/wa7son" }, - "@newrelic/security-agent@1.3.0": { + "@newrelic/security-agent@1.4.0": { "name": "@newrelic/security-agent", - "version": "1.3.0", + "version": "1.4.0", "range": "^1.3.0", "licenses": "UNKNOWN", "repoUrl": "https://github.com/newrelic/csec-node-agent", - "versionedRepoUrl": "https://github.com/newrelic/csec-node-agent/tree/v1.3.0", + "versionedRepoUrl": "https://github.com/newrelic/csec-node-agent/tree/v1.4.0", "licenseFile": "node_modules/@newrelic/security-agent/LICENSE", - "licenseUrl": "https://github.com/newrelic/csec-node-agent/blob/v1.3.0/LICENSE", + "licenseUrl": "https://github.com/newrelic/csec-node-agent/blob/v1.4.0/LICENSE", "licenseTextSource": "file", "publisher": "newrelic" }, @@ -120,29 +120,29 @@ "publisher": "Max Ogden", "email": "max@maxogden.com" }, - "https-proxy-agent@7.0.4": { + "https-proxy-agent@7.0.5": { "name": "https-proxy-agent", - "version": "7.0.4", + "version": "7.0.5", "range": "^7.0.1", "licenses": "MIT", "repoUrl": "https://github.com/TooTallNate/proxy-agents", - "versionedRepoUrl": "https://github.com/TooTallNate/proxy-agents/tree/v7.0.4", + "versionedRepoUrl": "https://github.com/TooTallNate/proxy-agents/tree/v7.0.5", "licenseFile": "node_modules/https-proxy-agent/LICENSE", - "licenseUrl": "https://github.com/TooTallNate/proxy-agents/blob/v7.0.4/LICENSE", + "licenseUrl": "https://github.com/TooTallNate/proxy-agents/blob/v7.0.5/LICENSE", "licenseTextSource": "file", "publisher": "Nathan Rajlich", "email": "nathan@tootallnate.net", "url": "http://n8.io/" }, - "import-in-the-middle@1.8.0": { + "import-in-the-middle@1.9.1": { "name": "import-in-the-middle", - "version": "1.8.0", + "version": "1.9.1", "range": "^1.6.0", "licenses": "Apache-2.0", - "repoUrl": "https://github.com/DataDog/import-in-the-middle", - "versionedRepoUrl": "https://github.com/DataDog/import-in-the-middle/tree/v1.8.0", + "repoUrl": "https://github.com/nodejs/import-in-the-middle", + "versionedRepoUrl": "https://github.com/nodejs/import-in-the-middle/tree/v1.9.1", "licenseFile": "node_modules/import-in-the-middle/LICENSE", - "licenseUrl": "https://github.com/DataDog/import-in-the-middle/blob/v1.8.0/LICENSE", + "licenseUrl": "https://github.com/nodejs/import-in-the-middle/blob/v1.9.1/LICENSE", "licenseTextSource": "file", "publisher": "Bryan English", "email": "bryan.english@datadoghq.com" @@ -199,55 +199,55 @@ "licenseUrl": "https://github.com/nodejs/readable-stream/blob/v3.6.2/LICENSE", "licenseTextSource": "file" }, - "semver@7.6.2": { + "semver@7.6.3": { "name": "semver", - "version": "7.6.2", + "version": "7.6.3", "range": "^7.5.2", "licenses": "ISC", "repoUrl": "https://github.com/npm/node-semver", - "versionedRepoUrl": "https://github.com/npm/node-semver/tree/v7.6.2", + "versionedRepoUrl": "https://github.com/npm/node-semver/tree/v7.6.3", "licenseFile": "node_modules/semver/LICENSE", - "licenseUrl": "https://github.com/npm/node-semver/blob/v7.6.2/LICENSE", + "licenseUrl": "https://github.com/npm/node-semver/blob/v7.6.3/LICENSE", "licenseTextSource": "file", "publisher": "GitHub Inc." }, - "winston-transport@4.7.0": { + "winston-transport@4.7.1": { "name": "winston-transport", - "version": "4.7.0", + "version": "4.7.1", "range": "^4.5.0", "licenses": "MIT", "repoUrl": "https://github.com/winstonjs/winston-transport", - "versionedRepoUrl": "https://github.com/winstonjs/winston-transport/tree/v4.7.0", + "versionedRepoUrl": "https://github.com/winstonjs/winston-transport/tree/v4.7.1", "licenseFile": "node_modules/winston-transport/LICENSE", - "licenseUrl": "https://github.com/winstonjs/winston-transport/blob/v4.7.0/LICENSE", + "licenseUrl": "https://github.com/winstonjs/winston-transport/blob/v4.7.1/LICENSE", "licenseTextSource": "file", "publisher": "Charlie Robbins", "email": "charlie.robbins@gmail.com" } }, "devDependencies": { - "@aws-sdk/client-s3@3.592.0": { + "@aws-sdk/client-s3@3.616.0": { "name": "@aws-sdk/client-s3", - "version": "3.592.0", + "version": "3.616.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.592.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.616.0", "licenseFile": "node_modules/@aws-sdk/client-s3/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.592.0/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.616.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" }, - "@aws-sdk/s3-request-presigner@3.592.0": { + "@aws-sdk/s3-request-presigner@3.616.0": { "name": "@aws-sdk/s3-request-presigner", - "version": "3.592.0", + "version": "3.616.0", "range": "^3.556.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js-v3", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.592.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js-v3/tree/v3.616.0", "licenseFile": "node_modules/@aws-sdk/s3-request-presigner/LICENSE", - "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.592.0/LICENSE", + "licenseUrl": "https://github.com/aws/aws-sdk-js-v3/blob/v3.616.0/LICENSE", "licenseTextSource": "file", "publisher": "AWS SDK for JavaScript Team", "url": "https://aws.amazon.com/javascript/" @@ -290,15 +290,15 @@ "licenseTextSource": "file", "publisher": "New Relic" }, - "@newrelic/test-utilities@8.6.0": { + "@newrelic/test-utilities@8.7.0": { "name": "@newrelic/test-utilities", - "version": "8.6.0", + "version": "8.7.0", "range": "^8.5.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/newrelic/node-test-utilities", - "versionedRepoUrl": "https://github.com/newrelic/node-test-utilities/tree/v8.6.0", + "versionedRepoUrl": "https://github.com/newrelic/node-test-utilities/tree/v8.7.0", "licenseFile": "node_modules/@newrelic/test-utilities/LICENSE", - "licenseUrl": "https://github.com/newrelic/node-test-utilities/blob/v8.6.0/LICENSE", + "licenseUrl": "https://github.com/newrelic/node-test-utilities/blob/v8.7.0/LICENSE", "licenseTextSource": "file", "publisher": "New Relic Node.js agent team", "email": "nodejs@newrelic.com" @@ -314,15 +314,15 @@ "licenseUrl": "https://github.com/octokit/rest.js/blob/v18.12.0/LICENSE", "licenseTextSource": "file" }, - "@slack/bolt@3.18.0": { + "@slack/bolt@3.19.0": { "name": "@slack/bolt", - "version": "3.18.0", + "version": "3.19.0", "range": "^3.7.0", "licenses": "MIT", "repoUrl": "https://github.com/slackapi/bolt", - "versionedRepoUrl": "https://github.com/slackapi/bolt/tree/v3.18.0", + "versionedRepoUrl": "https://github.com/slackapi/bolt/tree/v3.19.0", "licenseFile": "node_modules/@slack/bolt/LICENSE", - "licenseUrl": "https://github.com/slackapi/bolt/blob/v3.18.0/LICENSE", + "licenseUrl": "https://github.com/slackapi/bolt/blob/v3.19.0/LICENSE", "licenseTextSource": "file", "publisher": "Slack Technologies, LLC" }, @@ -376,19 +376,32 @@ "licenseTextSource": "file", "publisher": "Caolan McMahon" }, - "aws-sdk@2.1636.0": { + "aws-sdk@2.1659.0": { "name": "aws-sdk", - "version": "2.1636.0", + "version": "2.1659.0", "range": "^2.1604.0", "licenses": "Apache-2.0", "repoUrl": "https://github.com/aws/aws-sdk-js", - "versionedRepoUrl": "https://github.com/aws/aws-sdk-js/tree/v2.1636.0", + "versionedRepoUrl": "https://github.com/aws/aws-sdk-js/tree/v2.1659.0", "licenseFile": "node_modules/aws-sdk/LICENSE.txt", - "licenseUrl": "https://github.com/aws/aws-sdk-js/blob/v2.1636.0/LICENSE.txt", + "licenseUrl": "https://github.com/aws/aws-sdk-js/blob/v2.1659.0/LICENSE.txt", "licenseTextSource": "file", "publisher": "Amazon Web Services", "url": "https://aws.amazon.com/" }, + "borp@0.17.0": { + "name": "borp", + "version": "0.17.0", + "range": "^0.17.0", + "licenses": "MIT", + "repoUrl": "https://github.com/mcollina/borp", + "versionedRepoUrl": "https://github.com/mcollina/borp/tree/v0.17.0", + "licenseFile": "node_modules/borp/LICENSE", + "licenseUrl": "https://github.com/mcollina/borp/blob/v0.17.0/LICENSE", + "licenseTextSource": "file", + "publisher": "Matteo Collina", + "email": "hello@matteocollina.com" + }, "c8@8.0.1": { "name": "c8", "version": "8.0.1", @@ -480,15 +493,15 @@ "publisher": "Michael Radionov", "url": "https://github.com/mradionov" }, - "eslint-plugin-jsdoc@48.2.8": { + "eslint-plugin-jsdoc@48.8.0": { "name": "eslint-plugin-jsdoc", - "version": "48.2.8", + "version": "48.8.0", "range": "^48.0.5", "licenses": "BSD-3-Clause", "repoUrl": "https://github.com/gajus/eslint-plugin-jsdoc", - "versionedRepoUrl": "https://github.com/gajus/eslint-plugin-jsdoc/tree/v48.2.8", + "versionedRepoUrl": "https://github.com/gajus/eslint-plugin-jsdoc/tree/v48.8.0", "licenseFile": "node_modules/eslint-plugin-jsdoc/LICENSE", - "licenseUrl": "https://github.com/gajus/eslint-plugin-jsdoc/blob/v48.2.8/LICENSE", + "licenseUrl": "https://github.com/gajus/eslint-plugin-jsdoc/blob/v48.8.0/LICENSE", "licenseTextSource": "file", "publisher": "Gajus Kuizinas", "email": "gajus@gajus.com", @@ -644,15 +657,15 @@ "publisher": "Andrey Okonetchnikov", "email": "andrey@okonet.ru" }, - "lockfile-lint@4.13.2": { + "lockfile-lint@4.14.0": { "name": "lockfile-lint", - "version": "4.13.2", + "version": "4.14.0", "range": "^4.9.6", "licenses": "Apache-2.0", "repoUrl": "https://github.com/lirantal/lockfile-lint", - "versionedRepoUrl": "https://github.com/lirantal/lockfile-lint/tree/v4.13.2", + "versionedRepoUrl": "https://github.com/lirantal/lockfile-lint/tree/v4.14.0", "licenseFile": "node_modules/lockfile-lint/LICENSE", - "licenseUrl": "https://github.com/lirantal/lockfile-lint/blob/v4.13.2/LICENSE", + "licenseUrl": "https://github.com/lirantal/lockfile-lint/blob/v4.14.0/LICENSE", "licenseTextSource": "file", "publisher": "Liran Tal", "email": "liran.tal@gmail.com", @@ -671,16 +684,16 @@ "publisher": "Pedro Teixeira", "email": "pedro.teixeira@gmail.com" }, - "proxy@2.1.1": { + "proxy@2.2.0": { "name": "proxy", - "version": "2.1.1", + "version": "2.2.0", "range": "^2.1.1", "licenses": "MIT", "repoUrl": "https://github.com/TooTallNate/proxy-agents", - "versionedRepoUrl": "https://github.com/TooTallNate/proxy-agents/tree/v2.1.1", - "licenseFile": "node_modules/proxy/README.md", - "licenseUrl": "https://github.com/TooTallNate/proxy-agents/blob/v2.1.1/README.md", - "licenseTextSource": "spdx", + "versionedRepoUrl": "https://github.com/TooTallNate/proxy-agents/tree/v2.2.0", + "licenseFile": "node_modules/proxy/LICENSE", + "licenseUrl": "https://github.com/TooTallNate/proxy-agents/blob/v2.2.0/LICENSE", + "licenseTextSource": "file", "publisher": "Nathan Rajlich", "email": "nathan@tootallnate.net", "url": "http://n8.io/" @@ -697,15 +710,15 @@ "licenseTextSource": "file", "publisher": "Thorsten Lorenz" }, - "rfdc@1.3.1": { + "rfdc@1.4.1": { "name": "rfdc", - "version": "1.3.1", + "version": "1.4.1", "range": "^1.3.1", "licenses": "MIT", "repoUrl": "https://github.com/davidmarkclements/rfdc", - "versionedRepoUrl": "https://github.com/davidmarkclements/rfdc/tree/v1.3.1", + "versionedRepoUrl": "https://github.com/davidmarkclements/rfdc/tree/v1.4.1", "licenseFile": "node_modules/rfdc/LICENSE", - "licenseUrl": "https://github.com/davidmarkclements/rfdc/blob/v1.3.1/LICENSE", + "licenseUrl": "https://github.com/davidmarkclements/rfdc/blob/v1.4.1/LICENSE", "licenseTextSource": "file", "publisher": "David Mark Clements", "email": "david.clements@nearform.com"