├── .babelrc ├── .circleci └── config.yml ├── .dockerignore ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .mailmap ├── .npmignore ├── .npmrc-ci ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── acceptance ├── handler.js └── invoke.js ├── docker-compose.yml ├── package.json ├── serverless.yml ├── src ├── __mocks__ │ ├── dns.js │ └── sendReport.js ├── __snapshots__ │ ├── class.test.js.snap │ └── plugins.test.js.snap ├── class.test.js ├── collector.js ├── collector.test.js ├── config │ ├── __mocks__ │ │ ├── package.json │ │ └── util.js │ ├── cosmi.js │ ├── default.js │ ├── environment.js │ ├── index.js │ ├── index.test.js │ ├── object.js │ └── util.js ├── constants.js ├── dns.js ├── fileUploadMeta │ ├── getSigner.js │ ├── getSigner.test.js │ ├── index.js │ └── index.test.js ├── globals.js ├── hooks.js ├── index.js ├── index.test.js ├── invocationContext.js ├── mockSystem.js ├── mockSystem.test.js ├── plugins.test.js ├── plugins │ ├── allHooks.js │ └── mock.js ├── report.js ├── report.test.js ├── schema.json ├── sendReport.js ├── system.js ├── util.js ├── util.test.js ├── util │ ├── setupPlugins.js │ ├── strToBool.js │ └── strToBool.test.js ├── uuidv4.js └── uuidv4.test.js ├── testProjects ├── catchTypeError │ ├── handler.js │ ├── handler.test.js │ ├── package.json │ └── yarn.lock ├── extendConfig │ ├── handler.test.js │ ├── package-lock.json │ ├── package.json │ └── yarn.lock ├── metaWithExtraPlugins │ ├── handler.test.js │ ├── package.json │ └── yarn.lock ├── networkTimeout │ ├── handler.test.js │ ├── package.json │ └── yarn.lock ├── packageJsonConfig │ ├── handler.test.js │ ├── package.json │ └── yarn.lock ├── promiseRejection │ ├── package.json │ ├── test.js │ └── yarn.lock ├── rcFileConfig │ ├── .iopiperc │ ├── handler.test.js │ ├── package.json │ └── yarn.lock └── util │ └── plugins.js ├── util ├── testProjects.js └── testUtils.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": ["4.3"] 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | ["transform-runtime", {"polyfill": false, "regenerator": true}] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | docker: 3 | - image: iopipe/circleci 4 | working_directory: ~/circleci-deployment 5 | 6 | version: 2 7 | 8 | jobs: 9 | install: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: yarn-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} 15 | - run: yarn 16 | - save_cache: 17 | paths: 18 | - node_modules 19 | key: yarn-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} 20 | 21 | test: 22 | <<: *defaults 23 | steps: 24 | - checkout 25 | - restore_cache: 26 | key: yarn-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} 27 | - run: yarn 28 | - run: yarn test 29 | # test also runs the build, so save the dist for later 30 | - save_cache: 31 | key: v1-dist-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 32 | paths: 33 | - dist 34 | 35 | acceptanceTests: 36 | <<: *defaults 37 | steps: 38 | - checkout 39 | - restore_cache: 40 | key: yarn-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} 41 | - restore_cache: 42 | key: v1-dist-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 43 | - run: npm run acceptance 44 | 45 | release: 46 | <<: *defaults 47 | steps: 48 | - checkout 49 | - restore_cache: 50 | key: yarn-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} 51 | - restore_cache: 52 | key: v1-dist-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 53 | - run: yarn run build 54 | - run: cp .npmrc-ci .npmrc 55 | - run: yarn run release 56 | 57 | workflows: 58 | version: 2 59 | all: 60 | jobs: 61 | - install 62 | - test: 63 | requires: 64 | - install 65 | - acceptanceTests: 66 | requires: 67 | - install 68 | - test 69 | filters: 70 | branches: 71 | only: master 72 | - release: 73 | requires: 74 | - install 75 | - test 76 | - acceptanceTests 77 | filters: 78 | branches: 79 | only: master 80 | tags: 81 | only: /^v.*/ 82 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | **Description** 15 | 16 | 19 | 20 | **Steps to reproduce the issue:** 21 | 1. 22 | 2. 23 | 3. 24 | 25 | **Describe the results you received:** 26 | 27 | 28 | **Describe the results you expected:** 29 | 30 | 31 | **Additional information you deem important (e.g. issue happens only occasionally):** 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .serverless 3 | node_modules 4 | coverage 5 | dist 6 | iopipe.js 7 | stats.json 8 | package-lock.json 9 | yarn-error.log 10 | # ignore vim swap files 11 | .*.sw* 12 | # ignore JetBrains-suite config files 13 | .idea 14 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Erica Windisch 2 | Erica Windisch 3 | Erica Windisch 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | spec 2 | acceptance 3 | coverage 4 | src 5 | -------------------------------------------------------------------------------- /.npmrc-ci: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Code of Conduct 4 | 5 | This project and everyone participating in it is governed by our (Code of Conduct)(https://github.com/iopipe/iopipe-docs/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to dev@iopipe.com. 6 | 7 | ## Working on an issue 8 | 9 | Comment on the issue (or, inside the organization, assign yourself to the issue) to clarify that you're starting work on the issue. 10 | 11 | Fork the repository and do work on your fork. 12 | 13 | Aim to include the issue number in the branch used to create your PR, to better assist if someone later is fetching from your remote. For example, fix-120 or issue/120 or fixing-report-bug-120 are all acceptable branch names. 14 | 15 | ### Committing & opening Prs 16 | 17 | We use GPG signing to sign and verify work from collaborators. See the [GitHub Documentation](https://help.github.com/articles/signing-commits-with-gpg/) for instructions on how to create a key and use it to sign your commits. Once this is done, your commits will show as verified within a pull request on GitHub. 18 | 19 | This project uses `semantic-release`, so ensure your commits are formatted appropriately for this purpose, and keep your git history clean. `yarn commit` is available in this repository to help you do this. 20 | 21 | When you are ready to push some code ensure: 22 | 23 | - Tests (including linter) pass 24 | - You've added tests for any new code 25 | 26 | When ready, open a [pull request](https://github.com/iopipe/iopipe-js-core/pulls) to the master branch of the project. 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:8 2 | 3 | WORKDIR /app 4 | 5 | RUN npm install -g yarn 6 | 7 | COPY . . 8 | 9 | RUN yarn 10 | RUN yarn test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of yright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2015-2017, Erica Windisch. IOpipe, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | https://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | DOCKER_COMPOSE_YML?=docker-compose.yml 4 | 5 | test: 6 | docker-compose -f ${DOCKER_COMPOSE_YML} build node 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IOpipe Agent for JavaScript 2 | -------------------------------------------- 3 | [![Coverage Status](https://coveralls.io/repos/github/iopipe/iopipe/badge.svg?branch=master)](https://coveralls.io/github/iopipe/iopipe?branch=master) 4 | [![npm version](https://badge.fury.io/js/%40iopipe%2Fcore.svg)](https://badge.fury.io/js/%40iopipe%2Fcore) 5 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 7 | 8 | [IOpipe](https://iopipe.com) is a serverless DevOps platform for organizations building event-driven architectures in [AWS Lambda](https://aws.amazon.com/lambda/). IOpipe captures crucial, high-fidelity metrics for each Lambda function invocation. This data powers a flexibile and robust development and operations experience with features including tracing, profiling, custom metrics, and low-latency alerts. Get started today to quickly and confidently gain superior observability, identify issues, and discover anomalies in your connected applications. 9 | 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Custom Metrics](#custom-metrics) 13 | - [Labels](#labels) 14 | - [Configuration](#configuration) 15 | - [Methods](#methods) 16 | - [Options](#options) 17 | 18 | __Note: this library is a lower-level implementation than the package [you might likely be looking for](https://github.com/iopipe/iopipe-js). Enjoy pre-bundled plugins like tracing and event info with [`@iopipe/iopipe`](https://github.com/iopipe/iopipe-js)__ 19 | 20 | # Installation 21 | 22 | Install using your package manager of choice, 23 | 24 | `npm install @iopipe/core` 25 | 26 | or 27 | 28 | `yarn add @iopipe/core` 29 | 30 | If you are using the Serverless Framework to deploy your lambdas, check out our [serverless plugin](https://github.com/iopipe/serverless-plugin-iopipe). 31 | 32 | # Usage 33 | 34 | Configure the library with your project token ([register for access](https://www.iopipe.com)), and it will automatically monitor and collect metrics from your applications running on AWS Lambda. 35 | 36 | Example: 37 | 38 | ```js 39 | const iopipeLib = require('@iopipe/core'); 40 | 41 | const iopipe = iopipeLib({ token: 'PROJECT_TOKEN' }); 42 | 43 | exports.handler = iopipe((event, context) => { 44 | context.succeed('This is my serverless function!'); 45 | }); 46 | ``` 47 | 48 | ## Custom metrics 49 | 50 | You may add custom metrics to an invocation using `context.iopipe.metric` to add 51 | either string or numerical values. Keys have a maximum length of 256 characters, and string values are limited 52 | to 1024. 53 | 54 | Example: 55 | 56 | ```js 57 | const iopipeLib = require('@iopipe/core'); 58 | 59 | const iopipe = iopipeLib({ token: 'PROJECT_TOKEN' }); 60 | 61 | exports.handler = iopipe((event, context) => { 62 | context.iopipe.metric('key', 'some-value'); 63 | context.iopipe.metric('another-key', 42); 64 | context.succeed('This is my serverless function!'); 65 | }); 66 | ``` 67 | 68 | ## Labels 69 | 70 | You can label invocations using `context.iopipe.label` to label an invocation with a string value, with a limit of 128 characters. 71 | 72 | Example: 73 | 74 | ```js 75 | const iopipeLib = require('@iopipe/core'); 76 | 77 | const iopipe = iopipeLib({ token: 'PROJECT_TOKEN' }); 78 | 79 | exports.handler = iopipe((event, context) => { 80 | context.iopipe.label('something-important-happened'); 81 | context.succeed('This is my serverless function!'); 82 | }); 83 | ``` 84 | 85 | # Configuration 86 | 87 | ## Methods 88 | 89 | You can configure your iopipe setup through one or more different methods - that can be mixed, providing a config chain. The current methods are listed below, in order of precendence. The module instantiation object overrides all other config values (if values are provided). 90 | 91 | 1. Module instantiation object 92 | 2. `IOPIPE_*` environment variables 93 | 3. [An `.iopiperc` file](#rc-file-configuration) 94 | 4. [An `iopipe` package.json entry](#packagejson-configuration) 95 | 5. [An `extends` key referencing a config package](#extends-configuration) 96 | 6. Default values 97 | 98 | ## Options 99 | 100 | #### `token` (string: required) 101 | 102 | If not supplied, the environment variable `$IOPIPE_TOKEN` will be used if present. [Find your project token](https://dashboard.iopipe.com/install). 103 | 104 | #### `debug` (bool: optional = false) 105 | 106 | Debug mode will log all data sent to IOpipe servers to STDOUT. This is also a good way to evaluate the sort of data that IOpipe is receiving from your application. If not supplied, the environment variable `$IOPIPE_DEBUG` will be used if present. 107 | 108 | ```js 109 | const iopipe = require('@iopipe/core')({ 110 | token: 'PROJECT_TOKEN', 111 | debug: true 112 | }); 113 | 114 | exports.handler = iopipe((event, context, callback) => { 115 | // Do things here. We'll log info to STDOUT. 116 | }); 117 | ``` 118 | 119 | #### `networkTimeout` (int: optional = 5000) 120 | 121 | The number of milliseconds IOpipe will wait while sending a report before timing out. If not supplied, the environment variable `$IOPIPE_NETWORK_TIMEOUT` will be used if present. 122 | 123 | ```js 124 | const iopipe = require('@iopipe/core')({ token: 'PROJECT_TOKEN', networkTimeout: 30000}) 125 | ``` 126 | 127 | #### `timeoutWindow` (int: optional = 150) 128 | 129 | By default, IOpipe will capture timeouts by exiting your function 150ms early from the AWS configured timeout, to allow time for reporting. You can disable this feature by setting `timeoutWindow` to `0` in your configuration. If not supplied, the environment variable `$IOPIPE_TIMEOUT_WINDOW` will be used if present. 130 | 131 | ```js 132 | const iopipe = require('@iopipe/core')({ token: 'PROJECT_TOKEN', timeoutWindow: 0}) 133 | ``` 134 | 135 | #### `plugins` (array: optional) 136 | 137 | _Note that [if you use the `@iopipe/iopipe` package](https://github.com/iopipe/iopipe-js), you get our recommended plugin set-up right away._ Plugins can extend the functionality of IOpipe in ways that best work for you. Follow the guides for the plugins listed below for proper installation and usage on the `@iopipe/core` library: 138 | 139 | - [Event Info Plugin](https://github.com/iopipe/iopipe-plugin-event-info) 140 | - [Trace Plugin](https://github.com/iopipe/iopipe-plugin-trace) 141 | - [Logger Plugin](https://github.com/iopipe/iopipe-plugin-logger) 142 | - [Profiler Plugin](https://github.com/iopipe/iopipe-plugin-profiler) 143 | 144 | Example: 145 | 146 | ```js 147 | const tracePlugin = require('@iopipe/trace'); 148 | 149 | const iopipe = require('@iopipe/core')({ 150 | token: 'PROJECT_TOKEN', 151 | plugins: [tracePlugin()] 152 | }); 153 | 154 | exports.handler = iopipe((event, context, callback) => { 155 | // Run your fn here 156 | }); 157 | ``` 158 | 159 | #### `enabled` (boolean: optional = True) 160 | 161 | Conditionally enable/disable the agent. The environment variable `$IOPIPE_ENABLED` will also be checked. 162 | 163 | #### `url` (string: optional) 164 | 165 | Sets an alternative URL to use for the IOpipe collector. The environment variable `$IOPIPE_COLLECTOR_URL` will be used if present. 166 | 167 | ## RC File Configuration 168 | Not recommended for webpack/bundlers due to dynamic require. 169 | 170 | You can configure iopipe via an `.iopiperc` RC file. [An example of that is here](https://github.com/iopipe/iopipe-js-core/blob/master/testProjects/rcFileConfig/.iopiperc). Config options are the same as the module instantiation object, except for plugins. Plugins should be an array containing mixed-type values. A plugin value can be a: 171 | - String that is the name of the plugin 172 | - Or an array with plugin name first, and plugin options second 173 | 174 | ```json 175 | { 176 | "token": "wow_token", 177 | "plugins": [ 178 | "@iopipe/trace", 179 | ["@iopipe/profiler", {"enabled": true}] 180 | ] 181 | } 182 | ``` 183 | 184 | **IMPORTANT**: You must install the plugins as dependencies for them to load properly in your environment. 185 | 186 | ## package.json Configuration 187 | Not recommended for webpack/bundlers due to dynamic require. 188 | 189 | You can configure iopipe within a `iopipe` package.json entry. [An example of that is here](https://github.com/iopipe/iopipe/blob/master/testProjects/packageJsonConfig/package.json#L10). Config options are the same as the module instantiation object, except for plugins. Plugins should be an array containing mixed-type values. A plugin value can be a: 190 | - String that is the name of the plugin 191 | - Or an array with plugin name first, and plugin options second 192 | 193 | ```json 194 | { 195 | "name": "my-great-package", 196 | "dependencies": { 197 | "@iopipe/trace": "^0.2.0", 198 | "@iopipe/profiler": "^0.1.0" 199 | }, 200 | "iopipe": { 201 | "token": "wow_token", 202 | "plugins": [ 203 | "@iopipe/trace", 204 | ["@iopipe/profiler", {"enabled": true}] 205 | ] 206 | } 207 | } 208 | ``` 209 | 210 | **IMPORTANT**: You must install the plugins as dependencies for them to load properly in your environment. 211 | 212 | ## Extends Configuration 213 | Not recommended for webpack/bundlers due to dynamic require. 214 | 215 | You can configure iopipe within a package.json or rc file by referencing a `extends` config package. [An example of that is here](https://github.com/iopipe/iopipe-js-core/blob/master/testProjects/extendConfig/package.json#L15). Config options are the same as the module instantiation object, except for plugins. Plugins should be an array containing mixed-type values. A plugin value can be a: 216 | - String that is the name of the plugin 217 | - Or an array with plugin name first, and plugin options second 218 | 219 | For an example of a config package, check out [@iopipe/config](https://github.com/iopipe/iopipe-js-config). 220 | 221 | **IMPORTANT**: You must install the config package and plugins as dependencies for them to load properly in your environment. 222 | 223 | # License 224 | 225 | Apache 2.0 226 | -------------------------------------------------------------------------------- /acceptance/handler.js: -------------------------------------------------------------------------------- 1 | const iopipe = require('../dist/iopipe')({ 2 | debug: true, 3 | token: process.env.IOPIPE_TOKEN || 'testSuite' 4 | }); 5 | 6 | module.exports.callback = iopipe((event, context, callback) => { 7 | context.iopipe.log('custom_metric', 'A custom metric for callback'); 8 | context.iopipe.metric( 9 | 'custom_metric_with_metric', 10 | 'A custom metric for callback' 11 | ); 12 | context.iopipe.label('callback'); 13 | callback(null, 'callback'); 14 | }); 15 | 16 | module.exports.succeed = iopipe((event, context) => { 17 | context.iopipe.log('custom_metric', 'A custom metric for succeed'); 18 | context.iopipe.metric( 19 | 'custom_metric_with_metric', 20 | 'A custom metric for succeed' 21 | ); 22 | context.iopipe.label('succeed'); 23 | context.succeed('context.succeed'); 24 | }); 25 | 26 | module.exports.fail = iopipe((event, context) => { 27 | context.iopipe.log('custom_metric', 'A custom metric for fail'); 28 | context.iopipe.metric( 29 | 'custom_metric_with_metric', 30 | 'A custom metric for fail' 31 | ); 32 | context.iopipe.label('fail'); 33 | context.fail('context.fail'); 34 | }); 35 | 36 | module.exports.done = iopipe((event, context) => { 37 | context.iopipe.log('custom_metric', 'A custom metric for done'); 38 | context.iopipe.metric( 39 | 'custom_metric_with_metric', 40 | 'A custom metric for done' 41 | ); 42 | context.iopipe.label('done'); 43 | context.done(null, 'context.done'); 44 | }); 45 | -------------------------------------------------------------------------------- /acceptance/invoke.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const every = require('lodash.every'); 3 | 4 | const lambda = new AWS.Lambda(); 5 | 6 | const awsId = process.env.AWS_ID; 7 | if (!awsId) { 8 | throw new Error('No AWS account id supplied to process.env.AWS_ID'); 9 | } 10 | 11 | const base = `arn:aws:lambda:us-west-2:${awsId}:function:iopipe-lib-master-acceptance-test-prod-`; 12 | 13 | const arns = ['callback', 'contextSuccess', 'contextFail', 'contextDone'].map( 14 | str => `${base}${str}` 15 | ); 16 | 17 | /*eslint-disable no-console*/ 18 | /*eslint-disable no-process-exit*/ 19 | 20 | function executeUploaded() { 21 | console.log('Running acceptance test invocations'); 22 | Promise.all( 23 | arns.map(arn => { 24 | return lambda 25 | .invoke({ 26 | InvocationType: 'RequestResponse', 27 | FunctionName: arn, 28 | Payload: JSON.stringify({ test: true }) 29 | }) 30 | .promise(); 31 | }) 32 | ) 33 | .then(([fn1, fn2, fn3, fn4]) => { 34 | const bool = every([ 35 | fn1.StatusCode === 200, 36 | fn1.Payload === '"callback"', 37 | fn2.StatusCode === 200, 38 | fn2.Payload === '"context.succeed"', 39 | fn3.StatusCode === 200, 40 | fn3.FunctionError === 'Handled', 41 | fn3.Payload === JSON.stringify({ errorMessage: 'context.fail' }), 42 | fn4.StatusCode === 200, 43 | fn4.Payload === '"context.done"' 44 | ]); 45 | if (bool) { 46 | console.log('Acceptance test passed.'); 47 | return process.exit(0); 48 | } 49 | console.error('Acceptance test failed.'); 50 | console.error('Results: ', JSON.stringify([fn1, fn2, fn3, fn4])); 51 | return process.exit(1); 52 | }) 53 | .catch(err => { 54 | console.error(err); 55 | process.exit(1); 56 | }); 57 | } 58 | 59 | executeUploaded(); 60 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | node: 4 | build: . 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iopipe/core", 3 | "version": "0.0.0-semantically-released", 4 | "description": "IOpipe Lambda Analytics & Tracing Agent", 5 | "main": "dist/iopipe.js", 6 | "scripts": { 7 | "acceptance": "npm run acceptanceDeploy && npm run acceptanceLocal && node acceptance/invoke", 8 | "acceptanceDeploy": "sls deploy", 9 | "acceptanceLocalCallback": "sls invoke local -f callback", 10 | "acceptanceLocalContextSuccess": "sls invoke local -f contextSuccess", 11 | "acceptanceLocalContextDone": "sls invoke local -f contextDone", 12 | "acceptanceLocal": "npm run acceptanceLocalCallback && npm run acceptanceLocalContextSuccess && npm run acceptanceLocalContextDone", 13 | "build": "npm run webpack", 14 | "commit": "iopipe-scripts commit", 15 | "lint": "iopipe-scripts lint", 16 | "jest": "iopipe-scripts test", 17 | "prepare": "npm run build", 18 | "prepublishOnly": "npm run build", 19 | "release": "iopipe-scripts release", 20 | "test": "npm run lint && npm run build && npm run jest && npm run testProjects", 21 | "testProjects": "node util/testProjects", 22 | "webpack": "webpack --progress --profile --colors" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/iopipe/iopipe.git" 27 | }, 28 | "files": [ 29 | "dist/" 30 | ], 31 | "keywords": [ 32 | "serverless", 33 | "agent", 34 | "analytics", 35 | "metrics", 36 | "telemetry", 37 | "tracing", 38 | "distributed tracing" 39 | ], 40 | "author": "IOpipe ", 41 | "license": "Apache-2.0", 42 | "bugs": { 43 | "url": "https://github.com/iopipe/iopipe/issues" 44 | }, 45 | "homepage": "https://github.com/iopipe/iopipe#readme", 46 | "engines": { 47 | "node": ">=4.3.2" 48 | }, 49 | "devDependencies": { 50 | "@iopipe/config": "^0.3.0", 51 | "@iopipe/scripts": "^1.4.1", 52 | "@iopipe/trace": "^0.3.0", 53 | "aws-lambda-mock-context": "^3.0.0", 54 | "aws-sdk": "^2.164.0", 55 | "babel-core": "^6.25.0", 56 | "babel-loader": "^7.1.1", 57 | "babel-plugin-transform-runtime": "^6.23.0", 58 | "babel-preset-env": "^1.6.0", 59 | "babel-runtime": "^6.26.0", 60 | "coveralls": "^3.0.4", 61 | "cross-spawn": "^6.0.4", 62 | "delay": "^2.0.0", 63 | "flat": "^2.0.1", 64 | "fs-extra": "^5.0.0", 65 | "is-ip": "^2.0.0", 66 | "istanbul": "^0.4.4", 67 | "lodash": "^4.17.4", 68 | "lodash.every": "^4.6.0", 69 | "nock": "^9.4.1", 70 | "pre-commit": "^1.2.2", 71 | "serverless": "^1.37.1", 72 | "webpack": "^3.2.0", 73 | "webpack-bundle-analyzer": "^3.3.2", 74 | "webpack-node-externals": "^1.6.0", 75 | "yargs": "^11.0.0" 76 | }, 77 | "pre-commit": [ 78 | "test" 79 | ], 80 | "jest": { 81 | "testPathIgnorePatterns": [ 82 | "node_modules/", 83 | "dist/", 84 | "testProjects/" 85 | ] 86 | }, 87 | "dependencies": { 88 | "cosmiconfig": "^4", 89 | "lodash.uniqby": "^4.7.0", 90 | "simple-get": "^3.0.2" 91 | }, 92 | "eslintConfig": { 93 | "extends": "./node_modules/@iopipe/scripts/eslint.js", 94 | "rules": { 95 | "import/prefer-default-export": 0 96 | } 97 | }, 98 | "eslintIgnore": [ 99 | "coverage", 100 | "node_modules", 101 | "dist", 102 | "iopipe.js", 103 | "acceptance/node_modules", 104 | "acceptance/iopipe.js", 105 | "testProjects/*/node_modules", 106 | "testProjects/*/iopipe.js" 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: iopipe-lib-master-acceptance-test 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | stage: prod 7 | region: us-west-2 8 | role: ${env:IAM_ROLE} 9 | 10 | functions: 11 | callback: 12 | handler: acceptance/handler.callback 13 | environment: 14 | IOPIPE_TOKEN: ${env:IOPIPE_TOKEN} 15 | events: 16 | - schedule: rate(10 minutes) 17 | contextSuccess: 18 | handler: acceptance/handler.succeed 19 | environment: 20 | IOPIPE_TOKEN: ${env:IOPIPE_TOKEN} 21 | events: 22 | - schedule: rate(10 minutes) 23 | contextFail: 24 | handler: acceptance/handler.fail 25 | environment: 26 | IOPIPE_TOKEN: ${env:IOPIPE_TOKEN} 27 | events: 28 | - schedule: rate(10 minutes) 29 | contextDone: 30 | handler: acceptance/handler.done 31 | environment: 32 | IOPIPE_TOKEN: ${env:IOPIPE_TOKEN} 33 | events: 34 | - schedule: rate(10 minutes) 35 | -------------------------------------------------------------------------------- /src/__mocks__/dns.js: -------------------------------------------------------------------------------- 1 | import dns from 'dns'; 2 | 3 | const promiseInstances = []; 4 | 5 | function getDnsPromise(host) { 6 | const prom = new Promise((resolve, reject) => { 7 | dns.lookup(host, (err, address) => { 8 | if (err) { 9 | reject(err); 10 | } 11 | resolve(address); 12 | }); 13 | }); 14 | promiseInstances.push(prom); 15 | return prom; 16 | } 17 | 18 | export { getDnsPromise, promiseInstances }; 19 | -------------------------------------------------------------------------------- /src/__mocks__/sendReport.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const reports = []; 4 | 5 | function sendReport(requestBody, config, ipAddress) { 6 | return new Promise(resolve => { 7 | const data = _.assign({}, requestBody, { 8 | _meta: { 9 | config, 10 | ipAddress 11 | } 12 | }); 13 | // use a timeout to emulate some amount of network latency for the report send 14 | // especially useful for class.test.js - proper timeout reporting 15 | setTimeout(() => { 16 | reports.push(data); 17 | resolve({ 18 | status: 200 19 | }); 20 | }, 10); 21 | }); 22 | } 23 | 24 | export { reports, sendReport }; 25 | -------------------------------------------------------------------------------- /src/__snapshots__/class.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Allows ctx.iopipe.log and iopipe.log functionality 1`] = ` 4 | Array [ 5 | Object { 6 | "n": undefined, 7 | "name": "metric-1", 8 | "s": "foo", 9 | }, 10 | Object { 11 | "n": undefined, 12 | "name": "metric-2", 13 | "s": "true", 14 | }, 15 | Object { 16 | "n": undefined, 17 | "name": "metric-3", 18 | "s": "{\\"ding\\":\\"dong\\"}", 19 | }, 20 | Object { 21 | "n": undefined, 22 | "name": "metric-4", 23 | "s": "[\\"whoa\\"]", 24 | }, 25 | Object { 26 | "n": 100, 27 | "name": "metric-5", 28 | "s": undefined, 29 | }, 30 | Object { 31 | "n": undefined, 32 | "name": "metric-6", 33 | "s": "undefined", 34 | }, 35 | Object { 36 | "n": undefined, 37 | "name": "metric-7", 38 | "s": "true", 39 | }, 40 | ] 41 | `; 42 | 43 | exports[`ctx.iopipe.label adds labels to the labels array 1`] = ` 44 | Array [ 45 | "label-1", 46 | "label-2", 47 | ] 48 | `; 49 | 50 | exports[`ctx.iopipe.metric adds metrics to the custom_metrics array 1`] = ` 51 | Array [ 52 | Object { 53 | "n": undefined, 54 | "name": "metric-1", 55 | "s": "foo", 56 | }, 57 | Object { 58 | "n": undefined, 59 | "name": "metric-2", 60 | "s": "true", 61 | }, 62 | Object { 63 | "n": undefined, 64 | "name": "metric-3", 65 | "s": "{\\"ding\\":\\"dong\\"}", 66 | }, 67 | Object { 68 | "n": undefined, 69 | "name": "metric-4", 70 | "s": "[\\"whoa\\"]", 71 | }, 72 | Object { 73 | "n": 100, 74 | "name": "metric-5", 75 | "s": undefined, 76 | }, 77 | Object { 78 | "n": undefined, 79 | "name": "metric-6", 80 | "s": "NaN", 81 | }, 82 | Object { 83 | "n": undefined, 84 | "name": "metric-7", 85 | "s": "undefined", 86 | }, 87 | ] 88 | `; 89 | -------------------------------------------------------------------------------- /src/__snapshots__/plugins.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`All hooks are called successfully when a plugin uses them all 1`] = ` 4 | Array [ 5 | "context.hasRun:pre:setup", 6 | "context.hasRun:post:setup", 7 | "context.hasRun:pre:invoke", 8 | "context.hasRun:post:invoke", 9 | "context.hasRun:pre:report", 10 | "context.hasRun:post:report", 11 | ] 12 | `; 13 | 14 | exports[`Hooks have not changed 1`] = ` 15 | Array [ 16 | "pre:setup", 17 | "post:setup", 18 | "pre:invoke", 19 | "post:invoke", 20 | "pre:report", 21 | "post:report", 22 | ] 23 | `; 24 | 25 | exports[`Multiple plugins can be loaded and work 1`] = ` 26 | Array [ 27 | Object { 28 | "name": "mock-ok", 29 | "s": "neat", 30 | }, 31 | Object { 32 | "name": "mock-foo", 33 | "s": "bar", 34 | }, 35 | ] 36 | `; 37 | -------------------------------------------------------------------------------- /src/class.test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import mockContext from 'aws-lambda-mock-context'; 3 | import isIp from 'is-ip'; 4 | 5 | import * as dns from './dns'; 6 | import { reports } from './sendReport'; 7 | import { COLDSTART, setColdStart } from './globals'; 8 | import { get as getContext } from './invocationContext'; 9 | 10 | jest.mock('./dns'); 11 | jest.mock('./sendReport'); 12 | 13 | const iopipeLib = require('./index'); 14 | 15 | function createAgent(kwargs) { 16 | return iopipeLib( 17 | _.defaults(kwargs, { 18 | token: 'testSuite' 19 | }) 20 | ); 21 | } 22 | 23 | function fnGenerator(token, region, timeout, completedObj) { 24 | const iopipe = createAgent({ 25 | token, 26 | url: `https://metrics-api.${region}.iopipe.com` 27 | }); 28 | return iopipe(function Wrapper(event, ctx) { 29 | setTimeout(() => { 30 | completedObj[event.functionKey] = true; 31 | ctx.succeed(ctx.iopipe); 32 | }, timeout); 33 | }); 34 | } 35 | 36 | function runWrappedFunction(fnToRun, funcName) { 37 | const functionName = funcName || 'iopipe-lib-unit-tests'; 38 | const ctx = mockContext({ functionName }); 39 | const event = {}; 40 | return new Promise(resolve => { 41 | // not sure why eslint thinks that userFnReturnValue is not reassigned. 42 | /*eslint-disable prefer-const*/ 43 | let userFnReturnValue; 44 | /*eslint-enable prefer-const*/ 45 | function fnResolver(error, response) { 46 | return resolve({ 47 | ctx, 48 | response, 49 | error, 50 | userFnReturnValue 51 | }); 52 | } 53 | userFnReturnValue = fnToRun(event, ctx, fnResolver); 54 | ctx.Promise.then(success => fnResolver(null, success)).catch(fnResolver); 55 | }); 56 | } 57 | 58 | test('Coldstart is true on first invocation, can be set to false', () => { 59 | expect(COLDSTART).toBe(true); 60 | setColdStart(false); 61 | expect(COLDSTART).toBe(false); 62 | setColdStart(true); 63 | }); 64 | 65 | // this test should run first in this file due to the nature of testing the dns promises 66 | test('coldstarts use dns and label appropriately', async done => { 67 | // 'DNS promise is instantiated on library import, and reused for the coldstart invocation. New DNS promises are generated for subsequent invocations 68 | try { 69 | const { promiseInstances } = dns; 70 | expect(promiseInstances).toHaveLength(0); 71 | const iopipe = iopipeLib({ token: 'testSuite' }); 72 | expect(promiseInstances).toHaveLength(1); 73 | 74 | const runs = []; 75 | 76 | const wrappedFunction = iopipe((event, ctx) => { 77 | runs.push(1); 78 | ctx.succeed('Decorate'); 79 | }); 80 | 81 | const run1 = await runWrappedFunction(wrappedFunction, 'coldstart-test'); 82 | const coldstartTest = _.find( 83 | reports, 84 | obj => obj.aws.functionName === 'coldstart-test' 85 | ); 86 | expect(coldstartTest.coldstart).toBe(true); 87 | expect(coldstartTest.labels).toContain('@iopipe/coldstart'); 88 | expect(run1.response).toEqual('Decorate'); 89 | expect(runs).toHaveLength(1); 90 | expect(promiseInstances).toHaveLength(1); 91 | 92 | const run2 = await runWrappedFunction(wrappedFunction, 'coldstart-test2'); 93 | const coldstartTest2 = _.find( 94 | reports, 95 | obj => obj.aws.functionName === 'coldstart-test2' 96 | ); 97 | expect(coldstartTest2.coldstart).toBe(false); 98 | expect(coldstartTest2.labels).not.toContain('@iopipe/coldstart'); 99 | expect(run2.response).toEqual('Decorate'); 100 | expect(runs).toHaveLength(2); 101 | expect(promiseInstances).toHaveLength(2); 102 | 103 | done(); 104 | } catch (err) { 105 | throw err; 106 | } 107 | }); 108 | 109 | test('Reports use different IP addresses based on config', async () => { 110 | const completed = { 111 | f1: false, 112 | f2: false 113 | }; 114 | 115 | try { 116 | const wrappedFunction1 = fnGenerator( 117 | 'ip-test-1', 118 | 'us-west-1', 119 | 10, 120 | completed 121 | ); 122 | const wrappedFunction2 = fnGenerator( 123 | 'ip-test-2', 124 | 'us-west-2', 125 | 5, 126 | completed 127 | ); 128 | 129 | expect(completed.f1).toBe(false); 130 | expect(completed.f2).toBe(false); 131 | 132 | const [fn1, fn2] = await Promise.all( 133 | [wrappedFunction1, wrappedFunction2].map((fn, index) => { 134 | const ctx = mockContext(); 135 | fn({ functionKey: `f${index + 1}` }, ctx); 136 | return ctx.Promise; 137 | }) 138 | ); 139 | expect(completed.f1 && completed.f2).toBe(true); 140 | expect(fn1.config.clientId).toBe('ip-test-1'); 141 | expect(fn2.config.clientId).toBe('ip-test-2'); 142 | const ips = _.chain(reports) 143 | .filter(r => r.client_id.match(/ip-test/)) 144 | .map('_meta') 145 | .map('ipAddress') 146 | .value(); 147 | expect(_.isArray(ips)).toBe(true); 148 | expect(ips).toHaveLength(2); 149 | expect(_.every(ips, isIp)).toBe(true); 150 | expect(ips[0]).not.toEqual(ips[1]); 151 | } catch (err) { 152 | throw err; 153 | } 154 | }); 155 | 156 | test('Allows ctx.iopipe.log and iopipe.log functionality', async () => { 157 | expect.assertions(4); 158 | try { 159 | const iopipe = createAgent({}); 160 | const wrappedFunction = iopipe(function Wrapper(event, ctx) { 161 | ctx.iopipe.log('metric-1', 'foo'); 162 | ctx.iopipe.log('metric-2', true); 163 | ctx.iopipe.log('metric-3', { ding: 'dong' }); 164 | ctx.iopipe.log('metric-4', ['whoa']); 165 | ctx.iopipe.log('metric-5', 100); 166 | ctx.iopipe.log('metric-6'); 167 | // test deprecated iopipe.log too 168 | iopipe.log('metric-7', true); 169 | ctx.succeed('all done'); 170 | }); 171 | 172 | const context = mockContext({ functionName: 'log-test' }); 173 | wrappedFunction({}, context); 174 | const val = await context.Promise; 175 | expect(val).toEqual('all done'); 176 | 177 | const metrics = _.chain(reports) 178 | .find(obj => obj.aws.functionName === 'log-test') 179 | .get('custom_metrics') 180 | .value(); 181 | expect(_.isArray(metrics)).toBe(true); 182 | expect(metrics).toHaveLength(7); 183 | expect(metrics).toMatchSnapshot(); 184 | } catch (err) { 185 | throw err; 186 | } 187 | }); 188 | 189 | test('ctx.iopipe.metric adds metrics to the custom_metrics array', async () => { 190 | expect.assertions(5); 191 | try { 192 | const iopipe = createAgent({}); 193 | const wrappedFunction = iopipe(function Wrapper(event, ctx) { 194 | ctx.iopipe.metric('metric-1', 'foo'); 195 | ctx.iopipe.metric('metric-2', true); 196 | ctx.iopipe.metric('metric-3', { ding: 'dong' }); 197 | ctx.iopipe.metric('metric-4', ['whoa']); 198 | ctx.iopipe.metric('metric-5', 100); 199 | // NaN is saved as string 200 | ctx.iopipe.metric('metric-6', Number('foo')); 201 | ctx.iopipe.metric('metric-7'); 202 | // This is too long to be added 203 | ctx.iopipe.metric( 204 | new Array(130).join('a'), 205 | 'value not saved because key too long' 206 | ); 207 | ctx.succeed('all done'); 208 | }); 209 | 210 | const context = mockContext({ functionName: 'metric-test' }); 211 | wrappedFunction({}, context); 212 | const val = await context.Promise; 213 | expect(val).toEqual('all done'); 214 | 215 | const metrics = _.chain(reports) 216 | .find(obj => obj.aws.functionName === 'metric-test') 217 | .get('custom_metrics') 218 | .value(); 219 | expect(_.isArray(metrics)).toBe(true); 220 | expect(metrics).toHaveLength(7); 221 | expect(metrics).toMatchSnapshot(); 222 | 223 | // Check for autolabel because metrics were added 224 | const labels = _.chain(reports) 225 | .find(obj => obj.aws.functionName === 'metric-test') 226 | .get('labels') 227 | .value(); 228 | expect(labels.includes('@iopipe/metrics')).toBe(true); 229 | } catch (err) { 230 | throw err; 231 | } 232 | }); 233 | 234 | test('Autolabels do not cause the @iopipe/metrics label to be added', async () => { 235 | expect.assertions(3); 236 | try { 237 | const iopipe = createAgent({}); 238 | const wrappedFunction = iopipe(function Wrapper(event, ctx) { 239 | ctx.iopipe.metric('@iopipe/foo', 'some-value'); 240 | ctx.succeed('all done'); 241 | }); 242 | 243 | const context = mockContext({ functionName: 'auto-label-test' }); 244 | wrappedFunction({}, context); 245 | const val = await context.Promise; 246 | expect(val).toEqual('all done'); 247 | 248 | const labels = _.chain(reports) 249 | .find(obj => obj.aws.functionName === 'auto-label-test') 250 | .get('labels') 251 | .value(); 252 | 253 | expect(_.isArray(labels)).toBe(true); 254 | expect(labels).toHaveLength(0); 255 | } catch (err) { 256 | throw err; 257 | } 258 | }); 259 | 260 | test('ctx.iopipe.label adds labels to the labels array', async () => { 261 | expect.assertions(4); 262 | try { 263 | const iopipe = createAgent({}); 264 | const wrappedFunction = iopipe(function Wrapper(event, ctx) { 265 | ctx.iopipe.label('label-1'); 266 | ctx.iopipe.label('label-2'); 267 | // Non-strings are dropped 268 | ctx.iopipe.label(2); 269 | ctx.iopipe.label({ foo: 'bar' }); 270 | // This label is too long to be added 271 | ctx.iopipe.label(new Array(130).join('a')); 272 | ctx.succeed('all done'); 273 | }); 274 | 275 | const context = mockContext({ functionName: 'label-test' }); 276 | wrappedFunction({}, context); 277 | const val = await context.Promise; 278 | expect(val).toEqual('all done'); 279 | 280 | const labels = _.chain(reports) 281 | .find(obj => obj.aws.functionName === 'label-test') 282 | .get('labels') 283 | .value(); 284 | 285 | expect(_.isArray(labels)).toBe(true); 286 | expect(labels).toHaveLength(2); 287 | expect(labels).toMatchSnapshot(); 288 | } catch (err) { 289 | throw err; 290 | } 291 | }); 292 | 293 | test('Defining original context properties does not error if descriptors are undefined', done => { 294 | try { 295 | let doneData; 296 | 297 | const iopipe = createAgent({ 298 | token: 'context-props' 299 | }); 300 | const func = iopipe((event, ctx, callback) => { 301 | callback(null, 'woot'); 302 | }); 303 | 304 | func({}, { success: () => {} }, (err, data) => { 305 | if (err) { 306 | throw err; 307 | } 308 | doneData = data; 309 | expect(doneData).toBe('woot'); 310 | done(); 311 | }); 312 | } catch (err) { 313 | throw err; 314 | } 315 | }); 316 | 317 | class TimeoutTestPlugin { 318 | constructor(state) { 319 | this.hooks = { 320 | 'post:invoke': () => { 321 | state.postInvokeCalls++; 322 | } 323 | }; 324 | return this; 325 | } 326 | } 327 | 328 | test('When timing out, the lambda reports to iopipe, does not succeed, and reports timeout in aws', async () => { 329 | expect.assertions(4); 330 | let returnValue; 331 | const testState = { 332 | postInvokeCalls: 0 333 | }; 334 | try { 335 | const iopipe = createAgent({ 336 | timeoutWindow: 25, 337 | plugins: [() => new TimeoutTestPlugin(testState)] 338 | }); 339 | const wrappedFunction = iopipe((event, ctx) => { 340 | setTimeout(() => { 341 | ctx.succeed('all done'); 342 | }, 30); 343 | }); 344 | 345 | const lambdaTimeoutMillis = 50; 346 | const context = mockContext({ 347 | functionName: 'timeout-test', 348 | timeout: lambdaTimeoutMillis / 1000 349 | }); 350 | wrappedFunction({}, context); 351 | returnValue = await context.Promise; 352 | } catch (err) { 353 | // the report made it to iopipe 354 | try { 355 | const report = _.find( 356 | reports, 357 | obj => obj.aws.functionName === 'timeout-test' 358 | ); 359 | expect(report.labels).toContain('@iopipe/timeout'); 360 | // the lambda did not succeed 361 | expect(returnValue).toBeUndefined(); 362 | // the lambda timed out 363 | expect(err.message).toMatch('Task timed out'); 364 | expect(testState.postInvokeCalls).toBe(1); 365 | } catch (err2) { 366 | throw err2; 367 | } 368 | } 369 | }); 370 | 371 | test('Exposes getContext function which is undefined before + after invocation, populated with current context during invocation', async () => { 372 | try { 373 | expect(getContext()).toBeUndefined(); 374 | expect(_.isFunction(iopipeLib.getContext)).toBe(true); 375 | expect(iopipeLib.getContext()).toBeUndefined(); 376 | const iopipe = createAgent({ token: 'getContext' }); 377 | const wrappedFunction = iopipe(function Wrapper(event, ctx) { 378 | ctx.succeed(200); 379 | expect(getContext().functionName).toBe('getContext'); 380 | iopipeLib.getContext().iopipe.log('getContextMetric'); 381 | }); 382 | 383 | const context = mockContext({ functionName: 'getContext' }); 384 | wrappedFunction({}, context); 385 | expect(iopipeLib.getContext().functionName).toEqual('getContext'); 386 | const val = await context.Promise; 387 | expect(getContext()).toBeUndefined(); 388 | expect(iopipeLib.getContext()).toBeUndefined(); 389 | expect(val).toEqual(200); 390 | const metrics = _.chain(reports) 391 | .filter(r => r.client_id === 'getContext') 392 | .map('custom_metrics') 393 | .value(); 394 | expect(metrics).toEqual([[{ name: 'getContextMetric', s: 'undefined' }]]); 395 | } catch (err) { 396 | throw err; 397 | } 398 | }); 399 | 400 | test('Captures errors that are not instanceof Error', async () => { 401 | try { 402 | const iopipe = createAgent({ token: 'objectErrorHandling' }); 403 | const wrappedFunction = iopipe(function Wrapper(event, ctx) { 404 | ctx.fail({ foo: true }); 405 | }); 406 | 407 | const context = mockContext({ functionName: 'objectErrorHandling' }); 408 | wrappedFunction({}, context); 409 | try { 410 | await context.Promise; 411 | throw new Error('Test should fail by reaching this point'); 412 | } catch (err) { 413 | const report = _.find( 414 | reports, 415 | r => r.client_id === 'objectErrorHandling' 416 | ); 417 | const { name, message, stack } = report.errors; 418 | // all values should be truthy strings 419 | expect([name, message, stack].map(d => typeof d)).toEqual( 420 | _.fill(Array(3), 'string') 421 | ); 422 | expect(report.labels).toContain('@iopipe/error'); 423 | } 424 | } catch (err) { 425 | throw err; 426 | } 427 | }); 428 | -------------------------------------------------------------------------------- /src/collector.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import { join } from 'path'; 3 | import { SUPPORTED_REGIONS } from './constants'; 4 | 5 | function getCollectorPath(baseUrl) { 6 | if (!baseUrl) { 7 | return '/v0/event'; 8 | } 9 | const eventURL = url.parse(baseUrl); 10 | eventURL.pathname = join(eventURL.pathname, 'v0/event'); 11 | const { pathname, search } = eventURL; 12 | 13 | eventURL.pathname = join(pathname, 'v0/event'); 14 | eventURL.path = search ? pathname + search : pathname; 15 | return eventURL.path; 16 | } 17 | 18 | function getHostname(configUrl) { 19 | let regionString = ''; 20 | if (configUrl) { 21 | return url.parse(configUrl).hostname; 22 | } 23 | if (SUPPORTED_REGIONS.has(process.env.AWS_REGION)) { 24 | regionString = `.${process.env.AWS_REGION}`; 25 | } 26 | return `metrics-api${regionString}.iopipe.com`; 27 | } 28 | 29 | export { getHostname, getCollectorPath }; 30 | -------------------------------------------------------------------------------- /src/collector.test.js: -------------------------------------------------------------------------------- 1 | import { resetEnv } from '../util/testUtils'; 2 | import { getHostname, getCollectorPath } from './collector'; 3 | 4 | describe('configuring collector hostname', () => { 5 | beforeEach(() => { 6 | resetEnv(); 7 | }); 8 | 9 | test('returns a base hostname if nothing else', () => { 10 | expect(getHostname()).toBe('metrics-api.iopipe.com'); 11 | }); 12 | 13 | test('returns a configured url if provided in config object', () => { 14 | expect(getHostname('http://myurl')).toBe('myurl'); 15 | }); 16 | 17 | /*eslint-disable max-statements*/ 18 | test('switches based on the region set in env vars', () => { 19 | process.env.AWS_REGION = 'ap-northeast-1'; 20 | const apNortheast1Collector = getHostname('', {}); 21 | 22 | process.env.AWS_REGION = 'ap-northeast-2'; 23 | const apNortheast2Collector = getHostname('', {}); 24 | 25 | process.env.AWS_REGION = 'ap-south-1'; 26 | const apSouth1Collector = getHostname('', {}); 27 | 28 | process.env.AWS_REGION = 'ap-southeast-1'; 29 | const apSoutheast1Collector = getHostname('', {}); 30 | 31 | process.env.AWS_REGION = 'ap-southeast-2'; 32 | const apSoutheast2Collector = getHostname('', {}); 33 | 34 | process.env.AWS_REGION = 'ca-central-1'; 35 | const caCentral1Collector = getHostname('', {}); 36 | 37 | process.env.AWS_REGION = 'eu-central-1'; 38 | const euCentral1Collector = getHostname('', {}); 39 | 40 | process.env.AWS_REGION = 'eu-west-1'; 41 | const euWest1Collector = getHostname('', {}); 42 | 43 | process.env.AWS_REGION = 'eu-west-2'; 44 | const euWest2Collector = getHostname('', {}); 45 | 46 | process.env.AWS_REGION = 'us-east-2'; 47 | const east2Collector = getHostname('', {}); 48 | 49 | process.env.AWS_REGION = 'us-west-1'; 50 | const west1Collector = getHostname('', {}); 51 | 52 | process.env.AWS_REGION = 'us-west-2'; 53 | const west2Collector = getHostname('', {}); 54 | 55 | expect(apNortheast1Collector).toBe('metrics-api.ap-northeast-1.iopipe.com'); 56 | expect(apNortheast2Collector).toBe('metrics-api.ap-northeast-2.iopipe.com'); 57 | expect(apSouth1Collector).toBe('metrics-api.ap-south-1.iopipe.com'); 58 | expect(apSoutheast1Collector).toBe('metrics-api.ap-southeast-1.iopipe.com'); 59 | expect(apSoutheast2Collector).toBe('metrics-api.ap-southeast-2.iopipe.com'); 60 | expect(caCentral1Collector).toBe('metrics-api.ca-central-1.iopipe.com'); 61 | expect(euCentral1Collector).toBe('metrics-api.eu-central-1.iopipe.com'); 62 | expect(euWest1Collector).toBe('metrics-api.eu-west-1.iopipe.com'); 63 | expect(euWest2Collector).toBe('metrics-api.eu-west-2.iopipe.com'); 64 | expect(east2Collector).toBe('metrics-api.us-east-2.iopipe.com'); 65 | expect(west1Collector).toBe('metrics-api.us-west-1.iopipe.com'); 66 | expect(west2Collector).toBe('metrics-api.us-west-2.iopipe.com'); 67 | }); 68 | 69 | test('defaults if an uncovered region or malformed', () => { 70 | process.env.AWS_REGION = 'eu-west-3'; 71 | const euWest3Collector = getHostname('', {}); 72 | 73 | process.env.AWS_REGION = 'NotARegion'; 74 | const notRegionCollector = getHostname('', {}); 75 | 76 | process.env.AWS_REGION = ''; 77 | const emptyRegionCollector = getHostname('', {}); 78 | 79 | expect(euWest3Collector).toBe('metrics-api.iopipe.com'); 80 | expect(notRegionCollector).toBe('metrics-api.iopipe.com'); 81 | expect(emptyRegionCollector).toBe('metrics-api.iopipe.com'); 82 | }); 83 | }); 84 | 85 | describe('configuring path', () => { 86 | test('adds query strings to the path', () => { 87 | expect(getCollectorPath('http://myurl?foo')).toBe('/v0/event?foo'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/config/__mocks__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock-package", 3 | "iopipe": { 4 | "clientId": "foobar123", 5 | "debug": true, 6 | "installMethod": "package", 7 | "plugins": [ 8 | "iopipe" 9 | ], 10 | "timeoutWindow": 100, 11 | "url": "https://foo.bar.baz.iopipe.com/foo/bar" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/config/__mocks__/util.js: -------------------------------------------------------------------------------- 1 | let configPath; 2 | 3 | /*eslint-disable import/no-dynamic-require*/ 4 | 5 | function getCosmiConfig() { 6 | try { 7 | const config = require(configPath); 8 | 9 | if (typeof config === 'object' && typeof config.iopipe === 'object') { 10 | return config.iopipe; 11 | } 12 | } catch (err) { 13 | Function.prototype; // noop 14 | } 15 | 16 | return {}; 17 | } 18 | 19 | function requireFromString(src, args) { 20 | try { 21 | const mod = require(src); 22 | 23 | if (args && args.constructor === Array) return mod(...args); 24 | 25 | return mod(); 26 | } catch (err) { 27 | Function.prototype; // noop 28 | } 29 | 30 | return undefined; 31 | } 32 | 33 | function getPlugins(plugins) { 34 | if (typeof plugins !== 'object' || !Array.isArray(plugins)) return undefined; 35 | 36 | return plugins 37 | .map(plugin => { 38 | if (Array.isArray(plugin)) { 39 | // The array should have at least one item, which should be the 40 | // plugin package name. 41 | if (!plugin[0]) return undefined; 42 | 43 | return requireFromString(plugin[0], plugin.slice(1)); 44 | } 45 | 46 | return requireFromString(plugin); 47 | }) 48 | .filter(plugin => typeof plugin !== 'undefined'); 49 | } 50 | 51 | function setConfigPath(path) { 52 | configPath = path; 53 | } 54 | 55 | export { getCosmiConfig, getPlugins, requireFromString, setConfigPath }; 56 | -------------------------------------------------------------------------------- /src/config/cosmi.js: -------------------------------------------------------------------------------- 1 | import { getHostname, getCollectorPath } from './../collector'; 2 | 3 | import DefaultConfig from './default'; 4 | import { getCosmiConfig, requireFromString } from './util'; 5 | 6 | const classConfig = Symbol('cosmi'); 7 | 8 | export default class CosmiConfig extends DefaultConfig { 9 | /** 10 | * CosmiConfig configuration 11 | * 12 | * This class will attempt to load config values from an "iopipe" object if 13 | * found within the main package's package.json file. It will also attempt 14 | * to load values from an rc file if it exists. 15 | */ 16 | 17 | constructor() { 18 | super(); 19 | 20 | /* The extends object from the default of @iopipe/config */ 21 | const defaultExtendsObject = this.extends || {}; 22 | const cosmiObject = getCosmiConfig() || {}; 23 | /* If someone has {extends: "foo"} in their cosmiConfig (package.json, iopipe.rc) */ 24 | const cosmiExtendsObject = requireFromString(cosmiObject.extends) || {}; 25 | 26 | const plugins = [] 27 | .concat(cosmiObject.plugins) 28 | .concat(cosmiExtendsObject.plugins) 29 | .concat(defaultExtendsObject.plugins); 30 | 31 | this[classConfig] = Object.assign( 32 | {}, 33 | defaultExtendsObject, 34 | cosmiExtendsObject, 35 | cosmiObject, 36 | { 37 | plugins 38 | } 39 | ); 40 | } 41 | 42 | get clientId() { 43 | return ( 44 | this[classConfig].token || this[classConfig].clientId || super.clientId 45 | ); 46 | } 47 | 48 | get debug() { 49 | return this[classConfig].debug && 50 | typeof this[classConfig].debug === 'boolean' 51 | ? this[classConfig].debug 52 | : super.debug; 53 | } 54 | 55 | get extends() { 56 | return this[classConfig] && this[classConfig].extends 57 | ? this[classConfig].extends 58 | : super.extends; 59 | } 60 | 61 | get host() { 62 | return this[classConfig].url 63 | ? getHostname(this[classConfig].url) 64 | : super.host; 65 | } 66 | 67 | get installMethod() { 68 | return this[classConfig].installMethod 69 | ? this[classConfig].installMethod 70 | : super.installMethod; 71 | } 72 | 73 | get networkTimeout() { 74 | return this[classConfig].networkTimeout && 75 | Number.isInteger(this[classConfig].networkTimeout) 76 | ? this[classConfig].networkTimeout 77 | : super.networkTimeout; 78 | } 79 | 80 | get path() { 81 | return this[classConfig].url 82 | ? getCollectorPath(this[classConfig].url) 83 | : super.path; 84 | } 85 | 86 | get plugins() { 87 | return this[classConfig].plugins; 88 | } 89 | 90 | get timeoutWindow() { 91 | return this[classConfig].timeoutWindow && 92 | Number.isInteger(this[classConfig].timeoutWindow) 93 | ? this[classConfig].timeoutWindow 94 | : super.timeoutWindow; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/config/default.js: -------------------------------------------------------------------------------- 1 | import { getHostname, getCollectorPath } from './../collector'; 2 | 3 | let iopipeConfig; 4 | try { 5 | iopipeConfig = require('@iopipe/config'); 6 | } catch (err) { 7 | // noop 8 | } 9 | 10 | export default class DefaultConfig { 11 | /** 12 | * Default configuration 13 | * 14 | * This class should define sensible defaults for any supported config 15 | * values. 16 | */ 17 | 18 | get clientId() { 19 | return ''; 20 | } 21 | 22 | get debug() { 23 | return false; 24 | } 25 | 26 | get enabled() { 27 | return true; 28 | } 29 | 30 | get extends() { 31 | return iopipeConfig; 32 | } 33 | 34 | get host() { 35 | return getHostname(); 36 | } 37 | 38 | get installMethod() { 39 | return 'manual'; 40 | } 41 | 42 | get networkTimeout() { 43 | return 5000; 44 | } 45 | 46 | get path() { 47 | return getCollectorPath(); 48 | } 49 | 50 | get plugins() { 51 | return []; 52 | } 53 | 54 | get timeoutWindow() { 55 | return 150; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/config/environment.js: -------------------------------------------------------------------------------- 1 | import CosmiConfig from './cosmi'; 2 | 3 | const url = require('url'); 4 | 5 | export default class EnvironmentConfig extends CosmiConfig { 6 | /** 7 | * Environment variable configuration 8 | * 9 | * This class will look for IOPIPE_* environment variables and will atempt 10 | * to load them if present. 11 | */ 12 | 13 | get clientId() { 14 | return ( 15 | process.env.IOPIPE_TOKEN || process.env.IOPIPE_CLIENTID || super.clientId 16 | ); 17 | } 18 | 19 | get debug() { 20 | return process.env.IOPIPE_DEBUG 21 | ? ['true', 't', '1'].indexOf( 22 | process.env.IOPIPE_DEBUG.toString().toLowerCase() 23 | ) !== -1 24 | : super.debug; 25 | } 26 | 27 | get enabled() { 28 | return process.env.IOPIPE_ENABLED 29 | ? ['false', 'f', '0'].indexOf( 30 | process.env.IOPIPE_ENABLED.toString().toLowerCase() 31 | ) === -1 32 | : super.enabled; 33 | } 34 | 35 | get host() { 36 | return process.env.IOPIPE_COLLECTOR_URL 37 | ? url.parse(process.env.IOPIPE_COLLECTOR_URL).hostname 38 | : super.host; 39 | } 40 | 41 | get installMethod() { 42 | return process.env.IOPIPE_INSTALL_METHOD || super.installMethod; 43 | } 44 | 45 | get networkTimeout() { 46 | return Number.isInteger(parseInt(process.env.IOPIPE_NETWORK_TIMEOUT, 10)) 47 | ? parseInt(process.env.IOPIPE_NETWORK_TIMEOUT, 10) 48 | : super.networkTimeout; 49 | } 50 | 51 | get path() { 52 | return process.env.IOPIPE_COLLECTOR_URL 53 | ? url.parse(process.env.IOPIPE_COLLECTOR_URL).pathname 54 | : super.path; 55 | } 56 | 57 | get timeoutWindow() { 58 | return Number.isInteger(parseInt(process.env.IOPIPE_TIMEOUT_WINDOW, 10)) 59 | ? parseInt(process.env.IOPIPE_TIMEOUT_WINDOW, 10) 60 | : super.timeoutWindow; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import ObjectConfig from './object'; 2 | 3 | /** 4 | * Config Loader 5 | * 6 | * These classes load the agent config from a number of sources. They use 7 | * class inheritance to determine precedence of config values. 8 | * 9 | * Precedence Order: 10 | * 11 | * 1. Agent instantiation object. 12 | * 2. IOPIPE_* environment variables. 13 | * 3. An IOpipe RC file. 14 | * 4. A package.json with an "iopipe" object. 15 | * 5. The default values set in DefaultConfig. 16 | */ 17 | 18 | export default function setConfig(configObject) { 19 | return new ObjectConfig(configObject); 20 | } 21 | -------------------------------------------------------------------------------- /src/config/index.test.js: -------------------------------------------------------------------------------- 1 | import { resetEnv } from '../../util/testUtils'; 2 | import setConfig from './index'; 3 | import { setConfigPath } from './util'; 4 | 5 | jest.mock('./util'); 6 | jest.mock('@iopipe/config', () => { 7 | return { 8 | plugins: [] 9 | }; 10 | }); 11 | 12 | beforeEach(() => { 13 | resetEnv(); 14 | }); 15 | 16 | describe('setting up config object', () => { 17 | test('can accept 0 arguments and returns default config', () => { 18 | const config = setConfig(); 19 | 20 | expect(config.clientId).toEqual(''); 21 | 22 | expect(config.debug).toEqual(false); 23 | 24 | expect(config.enabled).toEqual(true); 25 | 26 | expect(config.host).toEqual('metrics-api.iopipe.com'); 27 | 28 | expect(config.installMethod).toEqual('manual'); 29 | 30 | expect(config.networkTimeout).toEqual(5000); 31 | 32 | expect(config.path).toEqual('/v0/event'); 33 | 34 | expect(config.plugins).toEqual([]); 35 | 36 | expect(config.timeoutWindow).toEqual(150); 37 | }); 38 | 39 | test('configures a client id', () => { 40 | expect(setConfig({ token: 'foo' }).clientId).toEqual('foo'); 41 | 42 | expect(setConfig({ clientId: 'bar' }).clientId).toEqual('bar'); 43 | }); 44 | 45 | test('sets preferences for order of client id config', () => { 46 | // takes token over clientId 47 | expect(setConfig({ clientId: 'bar', token: 'foo' }).clientId).toEqual( 48 | 'foo' 49 | ); 50 | 51 | process.env.IOPIPE_CLIENTID = 'qux'; 52 | 53 | expect(setConfig().clientId).toEqual('qux'); 54 | 55 | // takes IOPIPE_TOKEN over IOPIPE_CLIENTID 56 | process.env.IOPIPE_TOKEN = 'baz'; 57 | 58 | expect(setConfig().clientId).toEqual('baz'); 59 | }); 60 | 61 | test('sets timeout windows', () => { 62 | expect(setConfig({ timeoutWindow: 0 }).timeoutWindow).toEqual(0); 63 | 64 | process.env.IOPIPE_TIMEOUT_WINDOW = 100; 65 | 66 | expect(setConfig().timeoutWindow).toEqual(100); 67 | // prefers configuration over environment variables 68 | expect(setConfig({ timeoutWindow: 0 }).timeoutWindow).toEqual(0); 69 | }); 70 | 71 | test('can be configured via config file', () => { 72 | setConfigPath('./package'); 73 | 74 | const config = setConfig(); 75 | 76 | expect(config.clientId).toBe('foobar123'); 77 | 78 | expect(config.debug).toBe(true); 79 | 80 | expect(config.host).toBe('foo.bar.baz.iopipe.com'); 81 | 82 | expect(config.plugins).toHaveLength(1); 83 | 84 | expect(config.path).toBe('/foo/bar/v0/event'); 85 | 86 | expect(config.timeoutWindow).toBe(100); 87 | 88 | // instantiation config overrides package.json config 89 | expect(setConfig({ clientId: 'barbaz' }).clientId).toBe('barbaz'); 90 | 91 | process.env.IOPIPE_TOKEN = 'barbaz'; 92 | 93 | // Environment variables override package config 94 | expect(setConfig().clientId).toBe('barbaz'); 95 | }); 96 | 97 | test('can disable agent', () => { 98 | expect(setConfig({ enabled: false }).enabled).toBe(false); 99 | }); 100 | 101 | test('can disable agent via environment variable', () => { 102 | process.env.IOPIPE_ENABLED = '0'; 103 | 104 | expect(setConfig().enabled).toBe(false); 105 | 106 | process.env.IOPIPE_ENABLED = 'false'; 107 | 108 | expect(setConfig().enabled).toBe(false); 109 | }); 110 | 111 | test('should only be disabled explicitly', () => { 112 | process.env.IOPIPE_ENABLED = 'xyz'; 113 | 114 | expect(setConfig().enabled).toBe(true); 115 | 116 | process.env.IOPIPE_ENABLED = 'f'; 117 | 118 | expect(setConfig().enabled).toBe(false); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/config/object.js: -------------------------------------------------------------------------------- 1 | import { getHostname, getCollectorPath } from '../collector'; 2 | 3 | import EnvironmentConfig from './environment'; 4 | 5 | import { getPlugins, requireFromString } from './util'; 6 | 7 | const classConfig = Symbol('object'); 8 | 9 | export default class ObjectConfig extends EnvironmentConfig { 10 | /** 11 | * Config object configuration 12 | * 13 | * This class will accept a config object provided via agent instantiation 14 | * and will use any values that are present. 15 | */ 16 | 17 | constructor(opts = {}) { 18 | super(); 19 | 20 | const extendObject = requireFromString(opts.extends) || {}; 21 | this[classConfig] = Object.assign({}, extendObject, opts); 22 | return this; 23 | } 24 | 25 | get clientId() { 26 | return ( 27 | this[classConfig].token || this[classConfig].clientId || super.clientId 28 | ); 29 | } 30 | 31 | get debug() { 32 | return this[classConfig].debug || super.debug; 33 | } 34 | 35 | get enabled() { 36 | return typeof this[classConfig].enabled === 'boolean' 37 | ? this[classConfig].enabled 38 | : super.enabled; 39 | } 40 | 41 | get extends() { 42 | return this[classConfig] && this[classConfig].extends 43 | ? this[classConfig].extends 44 | : super.extends; 45 | } 46 | 47 | get host() { 48 | return this[classConfig].url 49 | ? getHostname(this[classConfig].url) 50 | : super.host; 51 | } 52 | 53 | get installMethod() { 54 | return this[classConfig].installMethod || super.installMethod; 55 | } 56 | 57 | get networkTimeout() { 58 | return this[classConfig].networkTimeout || super.networkTimeout; 59 | } 60 | 61 | get path() { 62 | return this[classConfig].url 63 | ? getCollectorPath(this[classConfig].url) 64 | : super.path; 65 | } 66 | 67 | get plugins() { 68 | const plugins = [].concat(this[classConfig].plugins).concat(super.plugins); 69 | return getPlugins(plugins); 70 | } 71 | 72 | get timeoutWindow() { 73 | return Number.isInteger(this[classConfig].timeoutWindow) 74 | ? this[classConfig].timeoutWindow 75 | : super.timeoutWindow; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/config/util.js: -------------------------------------------------------------------------------- 1 | import cosmiconfig from 'cosmiconfig'; 2 | 3 | /* Provides the `iopipe` object from main's `package.json` if it exists. Or returns values from an rc file if it exists. */ 4 | function getCosmiConfig() { 5 | try { 6 | const config = cosmiconfig('iopipe', { 7 | cache: false, 8 | sync: true, 9 | rcExtensions: true 10 | }).load(); 11 | 12 | if (config !== null) { 13 | return config.config; 14 | } 15 | } catch (err) { 16 | /*eslint-disable no-void*/ 17 | void 0; // noop 18 | } 19 | 20 | return {}; 21 | } 22 | 23 | /* 24 | * Attempts a require and instantiation from a given string. 25 | */ 26 | function requireFromString(src, args) { 27 | if (!src) { 28 | return undefined; 29 | } 30 | /*eslint-disable camelcase, no-undef*/ 31 | /* webpack bug: https://github.com/webpack/webpack/issues/5939 */ 32 | /* we should probably use guards like below, but we will skip them now due to the bug above*/ 33 | // const load = 34 | // typeof __non_webpack_require__ === 'function' 35 | // ? __non_webpack_require__ 36 | // : require; 37 | try { 38 | const mod = __non_webpack_require__(src); 39 | /*eslint-enable camelcase, no-undef*/ 40 | 41 | if (args && Array.isArray(args) && typeof mod === 'function') { 42 | return mod(...args); 43 | } 44 | 45 | if (typeof mod === 'function') { 46 | return mod(); 47 | } 48 | 49 | return mod; 50 | } catch (err) { 51 | /*eslint-disable no-console*/ 52 | console.warn(`Failed to import ${src}`); 53 | /*eslint-enable no-console*/ 54 | } 55 | 56 | return undefined; 57 | } 58 | 59 | /* 60 | * Returns plugins, instantiating with arguments if provided. 61 | */ 62 | function getPlugins(plugins) { 63 | if (typeof plugins !== 'object' || !Array.isArray(plugins)) return undefined; 64 | 65 | return plugins 66 | .filter(Boolean) 67 | .map(plugin => { 68 | if (Array.isArray(plugin)) { 69 | // The array should have at least one item, which should be the 70 | // plugin package name. 71 | if (!plugin[0]) return undefined; 72 | 73 | return requireFromString(plugin[0], plugin.slice(1)); 74 | } else if (typeof plugin === 'string') { 75 | return requireFromString(plugin); 76 | } 77 | return plugin; 78 | }) 79 | .filter(plugin => typeof plugin !== 'undefined'); 80 | } 81 | 82 | const setConfigPath = a => a; 83 | 84 | export { getCosmiConfig, getPlugins, requireFromString, setConfigPath }; 85 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // ⬇ these are typically the regions we have collectors in 2 | // we also support us-east-1 but due to DNS constraints we cannot list it here 3 | const SUPPORTED_REGIONS = new Map( 4 | [ 5 | 'ap-northeast-1', 6 | 'ap-northeast-2', 7 | 'ap-south-1', 8 | 'ap-southeast-1', 9 | 'ap-southeast-2', 10 | 'ca-central-1', 11 | 'eu-central-1', 12 | 'eu-west-1', 13 | 'eu-west-2', 14 | 'us-east-2', 15 | 'us-west-1', 16 | 'us-west-2' 17 | // construct the map where each key is a region and the value is true (supported) 18 | ].map(s => [s, true]) 19 | ); 20 | 21 | // we also support us-east-1 but due to DNS constraints we cannot list it here 22 | const SUPPORTED_SIGNER_REGIONS = new Map( 23 | [ 24 | 'ap-northeast-1', 25 | 'ap-southeast-2', 26 | 'eu-west-1', 27 | 'us-east-2', 28 | 'us-west-1', 29 | 'us-west-2' 30 | // construct the map where each key is a region and the value is true (supported) 31 | ].map(s => [s, true]) 32 | ); 33 | 34 | export { SUPPORTED_REGIONS, SUPPORTED_SIGNER_REGIONS }; 35 | -------------------------------------------------------------------------------- /src/dns.js: -------------------------------------------------------------------------------- 1 | import dns from 'dns'; 2 | 3 | const promiseInstances = undefined; 4 | 5 | function getDnsPromise(host) { 6 | return new Promise((resolve, reject) => { 7 | dns.lookup(host, (err, address) => { 8 | if (err) { 9 | reject(err); 10 | } 11 | resolve(address); 12 | }); 13 | }); 14 | } 15 | 16 | export { getDnsPromise, promiseInstances }; 17 | -------------------------------------------------------------------------------- /src/fileUploadMeta/getSigner.js: -------------------------------------------------------------------------------- 1 | import { SUPPORTED_SIGNER_REGIONS } from '../constants'; 2 | 3 | const supported = new Map(SUPPORTED_SIGNER_REGIONS); 4 | // allow us-east-1 for signer util 5 | supported.set('us-east-1', true); 6 | 7 | const fallbackSigners = { 8 | 'ap-northeast-2': 'ap-northeast-1', 9 | 'ap-south-1': 'ap-southeast-2', 10 | 'ap-southeast-1': 'ap-southeast-2', 11 | 'ap-southeast-2': 'ap-southeast-2', 12 | 'ca-central-1': 'us-east-2', 13 | 'eu-central-1': 'eu-west-1', 14 | 'eu-west-2': 'eu-west-1', 15 | 'eu-east-1': 'eu-east-1', 16 | 'us-west-1': 'us-west-1' 17 | }; 18 | 19 | export default function getSignerHostname() { 20 | const { AWS_REGION } = process.env; 21 | const signer = supported.has(AWS_REGION) 22 | ? AWS_REGION 23 | : fallbackSigners[AWS_REGION]; 24 | 25 | return `https://signer.${signer ? signer : 'us-west-2'}.iopipe.com/`; 26 | } 27 | -------------------------------------------------------------------------------- /src/fileUploadMeta/getSigner.test.js: -------------------------------------------------------------------------------- 1 | import lib from './getSigner'; 2 | 3 | beforeEach(() => { 4 | delete process.env.AWS_REGION; 5 | }); 6 | 7 | describe('getSigner', () => { 8 | test('Defaults to us-west-2', () => { 9 | expect(lib()).toEqual('https://signer.us-west-2.iopipe.com/'); 10 | }); 11 | 12 | test('Uses region if it is supported', () => { 13 | process.env.AWS_REGION = 'us-east-1'; 14 | expect(lib()).toEqual('https://signer.us-east-1.iopipe.com/'); 15 | }); 16 | 17 | test('Uses us-west-2 if region is not supported', () => { 18 | process.env.AWS_REGION = 'eu-west-3'; 19 | expect(lib()).toEqual('https://signer.us-west-2.iopipe.com/'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/fileUploadMeta/index.js: -------------------------------------------------------------------------------- 1 | import request from 'simple-get'; 2 | 3 | import { get } from '../invocationContext'; 4 | import getSigner from './getSigner'; 5 | 6 | export default function getFileUploadMeta(kwargs = {}) { 7 | return new Promise((resolve, reject) => { 8 | const context = get() || {}; 9 | const { 10 | arn = context.invokedFunctionArn || 11 | `arn:aws:lambda:local:0:function:${context.functionName}`, 12 | requestId = context.awsRequestId, 13 | timestamp = Date.now(), 14 | extension = '.zip', 15 | auth: authorization, 16 | method = 'POST', 17 | networkTimeout = 5000, 18 | url = getSigner() 19 | } = kwargs; 20 | 21 | const body = { arn, requestId, timestamp, extension }; 22 | let headers = {}; 23 | if (authorization) { 24 | headers = { authorization }; 25 | } 26 | request.concat( 27 | { url, body, headers, method, json: true, timeout: networkTimeout }, 28 | (err, res, data) => { 29 | return err ? reject(err) : resolve(data || {}); 30 | } 31 | ); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/fileUploadMeta/index.test.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import _ from 'lodash'; 3 | 4 | import { set } from '../invocationContext'; 5 | import lib from '.'; 6 | 7 | const requiredKeys = ['arn', 'requestId', 'timestamp', 'extension']; 8 | 9 | const hasRequiredKeys = obj => { 10 | return ( 11 | _.chain(obj) 12 | .pick(requiredKeys) 13 | .values() 14 | .compact() 15 | .value().length === requiredKeys.length 16 | ); 17 | }; 18 | 19 | const testResponse = { 20 | jwtAccess: 'jwt-here', 21 | signedRequest: 'aws.com/signed' 22 | }; 23 | 24 | beforeEach(() => { 25 | // set the context like the core lib would 26 | set({ 27 | invokedFunctionArn: 'test', 28 | awsRequestId: '1234' 29 | }); 30 | nock(/signer/) 31 | .post('/', hasRequiredKeys) 32 | .reply(200, testResponse); 33 | nock(/signer/) 34 | .post('/', b => !hasRequiredKeys(b)) 35 | .reply(500, { error: true }); 36 | }); 37 | 38 | afterEach(() => { 39 | // reset the context like the core lib would 40 | set(undefined); 41 | }); 42 | 43 | describe('fileUpload', () => { 44 | test('Works when supplied args', async () => { 45 | const val = await lib({ 46 | arn: 'wow', 47 | requestId: '1234', 48 | timestamp: 1530827699329, 49 | auth: 'auth-woot' 50 | }); 51 | expect(val).toEqual(testResponse); 52 | }); 53 | 54 | test('Works without supplied args', async () => { 55 | const val = await lib(); 56 | expect(val).toEqual(testResponse); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/globals.js: -------------------------------------------------------------------------------- 1 | import https from 'https'; 2 | import uuid from './uuidv4'; 3 | 4 | const pkg = require('../package.json'); 5 | 6 | // Default on module load; changed to false on first handler invocation. 7 | /*eslint-disable import/no-mutable-exports*/ 8 | /*eslint-disable prefer-const*/ 9 | let COLDSTART = true; 10 | /*eslint-enable prefer-const*/ 11 | 12 | const VERSION = pkg.version; 13 | const MODULE_LOAD_TIME = Date.now(); 14 | const PROCESS_ID = uuid(); 15 | 16 | function setColdStart(bool = COLDSTART) { 17 | COLDSTART = bool; 18 | } 19 | 20 | const httpsAgent = new https.Agent({ 21 | maxCachedSessions: 1, 22 | keepAlive: true 23 | }); 24 | 25 | httpsAgent.originalCreateConnection = httpsAgent.createConnection; 26 | httpsAgent.createConnection = (port, host, options) => { 27 | /* noDelay is documented as defaulting to true, but docs lie. 28 | this sacrifices throughput for latency and should be faster 29 | for how we submit data. */ 30 | const socket = httpsAgent.originalCreateConnection(port, host, options); 31 | socket.setNoDelay(true); 32 | return socket; 33 | }; 34 | 35 | export { 36 | VERSION, 37 | MODULE_LOAD_TIME, 38 | PROCESS_ID, 39 | COLDSTART, 40 | httpsAgent, 41 | setColdStart 42 | }; 43 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console*/ 2 | 3 | export const hooks = [ 4 | 'pre:setup', 5 | 'post:setup', 6 | 'pre:invoke', 7 | 'post:invoke', 8 | 'pre:report', 9 | 'post:report' 10 | ]; 11 | 12 | export function getHook(hook) { 13 | const val = hooks.find(str => str === hook); 14 | if (!val) { 15 | console.error(`Hook ${hook} not found.`); 16 | return 'none'; 17 | } 18 | return val; 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import setConfig from './config'; 2 | import Report from './report'; 3 | import { COLDSTART, VERSION } from './globals'; 4 | import { getDnsPromise } from './dns'; 5 | import { getHook } from './hooks'; 6 | import { convertToString } from './util'; 7 | import setupPlugins from './util/setupPlugins'; 8 | import getFileUploadMeta from './fileUploadMeta'; 9 | import { 10 | set as setInvocationContext, 11 | get as getInvocationContext 12 | } from './invocationContext'; 13 | 14 | /*eslint-disable no-console*/ 15 | 16 | function setupTimeoutCapture(wrapperInstance) { 17 | const { context, sendReport, config } = wrapperInstance; 18 | const { getRemainingTimeInMillis = () => 0 } = context; 19 | 20 | // if getRemainingTimeInMillis returns a very small number, it's probably a local invoke (sls framework perhaps) 21 | if (config.timeoutWindow < 1 || getRemainingTimeInMillis() < 10) { 22 | return undefined; 23 | } 24 | 25 | const maxEndTime = 899900; /* Maximum execution: 100ms short of 15 minutes */ 26 | const configEndTime = Math.max( 27 | 0, 28 | getRemainingTimeInMillis.call(context) - config.timeoutWindow 29 | ); 30 | 31 | const endTime = Math.min(configEndTime, maxEndTime); 32 | 33 | return setTimeout(() => { 34 | context.iopipe.label('@iopipe/timeout'); 35 | sendReport.call(wrapperInstance, new Error('Timeout Exceeded.'), () => {}); 36 | }, endTime); 37 | } 38 | 39 | process.on('unhandledRejection', error => { 40 | const ctx = getInvocationContext(); 41 | if (ctx && ctx.iopipe && ctx.iopipe.label) { 42 | // default node behavior is to log these types of errors 43 | console.error(error); 44 | ctx.iopipe.label('@iopipe/unhandled-promise-rejection'); 45 | ctx.iopipe.label('@iopipe/error'); 46 | } 47 | }); 48 | 49 | //TODO: refactor to abide by max-params rule*/ 50 | /*eslint-disable max-params*/ 51 | 52 | class IOpipeWrapperClass { 53 | constructor( 54 | originalIdentity, 55 | libFn, 56 | plugins = [], 57 | dnsPromise, 58 | config, 59 | userFunc, 60 | originalEvent, 61 | originalContext, 62 | originalCallback 63 | ) { 64 | this.startTime = process.hrtime(); 65 | this.startTimestamp = Date.now(); 66 | this.config = config; 67 | this.metrics = []; 68 | this.labels = new Set(); 69 | this.originalIdentity = originalIdentity; 70 | this.event = originalEvent; 71 | this.originalContext = originalContext; 72 | this.originalCallback = originalCallback; 73 | this.userFunc = userFunc; 74 | this.hasSentReport = false; 75 | 76 | this.plugins = setupPlugins(this, plugins); 77 | 78 | this.runHook('pre:setup'); 79 | 80 | // support deprecated iopipe.log 81 | libFn.log = (...logArgs) => { 82 | console.warn( 83 | 'iopipe.log is deprecated and will be removed in a future version, please use context.iopipe.metric' 84 | ); 85 | this.log(...logArgs); 86 | }; 87 | 88 | if (COLDSTART) { 89 | this.dnsPromise = dnsPromise; 90 | } else { 91 | // assign a new dnsPromise if it's not a coldstart because dns could have changed 92 | this.dnsPromise = getDnsPromise(this.config.host); 93 | } 94 | 95 | this.setupContext(); 96 | 97 | // assign modified methods and objects here 98 | this.context = Object.assign(this.originalContext, { 99 | // need to use .bind, otherwise, the this ref inside of each fn is NOT IOpipeWrapperClass 100 | succeed: this.succeed.bind(this), 101 | fail: this.fail.bind(this), 102 | done: this.done.bind(this), 103 | iopipe: { 104 | label: this.label.bind(this), 105 | log: this.log.bind(this), 106 | metric: this.metric.bind(this), 107 | version: VERSION, 108 | config: this.config 109 | } 110 | }); 111 | 112 | setInvocationContext(this.context); 113 | 114 | this.callback = (err, data) => { 115 | this.sendReport(err, () => { 116 | typeof this.originalCallback === 'function' && 117 | this.originalCallback(err, data); 118 | }); 119 | }; 120 | 121 | this.timeout = setupTimeoutCapture(this); 122 | this.report = new Report(this); 123 | 124 | this.runHook('post:setup'); 125 | 126 | return this; 127 | } 128 | setupContext(reset) { 129 | // preserve original functions via a property name change 130 | ['succeed', 'fail', 'done'].forEach(method => { 131 | const descriptor = Object.getOwnPropertyDescriptor( 132 | this.originalContext, 133 | reset ? `original_${method}` : method 134 | ); 135 | if (descriptor) { 136 | Object.defineProperty( 137 | this.originalContext, 138 | reset ? method : `original_${method}`, 139 | descriptor 140 | ); 141 | } 142 | delete this.originalContext[reset ? `original_${method}` : method]; 143 | }); 144 | } 145 | debugLog(message, level = 'warn') { 146 | if (this.config.debug) { 147 | console[level](message); 148 | } 149 | } 150 | invoke() { 151 | this.runHook('pre:invoke'); 152 | try { 153 | const result = this.userFunc.call( 154 | this.originalIdentity, 155 | this.event, 156 | this.context, 157 | this.callback 158 | ); 159 | if ( 160 | result && 161 | typeof result.then === 'function' && 162 | typeof result.catch === 'function' 163 | ) { 164 | return new Promise(resolve => { 165 | return result 166 | .then(value => { 167 | this.context.succeed(value); 168 | return this.callback(null, () => resolve(value)); 169 | }) 170 | .catch(err => { 171 | this.context.fail(err); 172 | return this.callback(err); 173 | }); 174 | }); 175 | } 176 | return result; 177 | } catch (err) { 178 | this.sendReport(err, () => this.originalCallback(err)); 179 | return err; 180 | } 181 | } 182 | async sendReport(err, cb = () => {}) { 183 | if (!this.hasSentReport) { 184 | this.hasSentReport = true; 185 | await this.runHook('post:invoke'); 186 | await this.report.prepare(err); 187 | await this.runHook('pre:report'); 188 | if (this.timeout) { 189 | clearTimeout(this.timeout); 190 | } 191 | this.report.send(async (...args) => { 192 | await this.runHook('post:report'); 193 | setInvocationContext(undefined); 194 | // reset the context back to its original state, otherwise aws gets unhappy 195 | this.setupContext(true); 196 | cb(...args); 197 | }); 198 | } 199 | } 200 | async runHook(hook) { 201 | const hookString = getHook(hook); 202 | const { plugins = [] } = this; 203 | try { 204 | await Promise.all( 205 | plugins.map(plugin => { 206 | try { 207 | const fn = plugin.hooks && plugin.hooks[hookString]; 208 | if (typeof fn === 'function') { 209 | return fn(this); 210 | } 211 | } catch (err) { 212 | // if this.config is undefined, the hook is probably pre:setup 213 | // lets error out if that is the case 214 | if (this.config === undefined || this.config.debug) { 215 | console.error(err); 216 | } 217 | } 218 | return Promise.resolve(); 219 | }) 220 | ); 221 | } catch (error) { 222 | if (this.config === undefined || this.config.debug) { 223 | console.error(error); 224 | } 225 | } 226 | } 227 | async succeed(data) { 228 | const context = this; 229 | if (data && typeof data.resolve === 'function') { 230 | await data.resolve(); 231 | } else if (data && typeof data.then === 'function') { 232 | return new Promise(resolve => { 233 | return data 234 | .then(value => { 235 | context.sendReport(null, () => { 236 | // eslint-disable-next-line no-undef 237 | context.originalContext.succeed(data); 238 | resolve(value); 239 | }); 240 | }) 241 | .catch(err => context.fail(err)); 242 | }); 243 | } 244 | return this.sendReport(null, () => { 245 | this.originalContext.succeed(data); 246 | }); 247 | } 248 | async fail(err) { 249 | const context = this; 250 | const handleErr = payload => 251 | this.sendReport(payload, () => context.originalContext.fail(payload)); 252 | 253 | if (err && typeof err.resolve === 'function') { 254 | await err.resolve(); 255 | } else if (typeof err.then === 'function') { 256 | return new Promise(() => { 257 | return err 258 | .then(value => handleErr(value)) 259 | .catch(errErr => handleErr(errErr)); 260 | }); 261 | } 262 | 263 | return this.sendReport(err, () => { 264 | this.originalContext.fail(err); 265 | }); 266 | } 267 | done(err, data) { 268 | this.sendReport(err, () => { 269 | this.originalContext.done(err, data); 270 | }); 271 | } 272 | metric(keyInput, valueInput) { 273 | let numberValue, stringValue; 274 | const key = convertToString(keyInput); 275 | if (key.length > 128) { 276 | this.debugLog( 277 | `Metric with key name ${key} is longer than allowed length of 128, metric will not be saved` 278 | ); 279 | return; 280 | } 281 | if (Number.isFinite(valueInput)) { 282 | numberValue = valueInput; 283 | } else { 284 | stringValue = convertToString(valueInput); 285 | } 286 | this.metrics.push({ 287 | name: key, 288 | n: numberValue, 289 | s: stringValue 290 | }); 291 | // Automatically label that this invocation contains metrics 292 | if (!key.startsWith('@iopipe')) { 293 | this.label('@iopipe/metrics'); 294 | } 295 | } 296 | label(name) { 297 | if (typeof name !== 'string') { 298 | this.debugLog(`Label ${name} is not a string and will not be saved`); 299 | return; 300 | } 301 | if (name.length > 128) { 302 | this.debugLog( 303 | `Label with name ${name} is longer than allowed length of 128, label will not be saved` 304 | ); 305 | return; 306 | } 307 | this.labels.add(name); 308 | } 309 | // DEPRECATED: This method is deprecated in favor of .metric and .label 310 | log(name, value) { 311 | this.debugLog( 312 | 'context.iopipe.log is deprecated and will be removed in a future version, please use context.iopipe.metric' 313 | ); 314 | this.metric(name, value); 315 | } 316 | } 317 | 318 | module.exports = options => { 319 | const config = setConfig(options); 320 | const { plugins } = config; 321 | 322 | const dnsPromise = getDnsPromise(config.host); 323 | const libFn = userFunc => { 324 | if (!config.enabled) { 325 | // No-op if agent is disabled 326 | return userFunc; 327 | } 328 | 329 | if (!config.clientId) { 330 | console.warn( 331 | 'Your function is wrapped with iopipe, but a valid token was not found. Methods such as iopipe.context.log will fail.' 332 | ); 333 | // No-op if user doesn't set an IOpipe token. 334 | return userFunc; 335 | } 336 | 337 | // Assign .log (deprecated) here to avoid type errors 338 | if (typeof libFn.log !== 'function') { 339 | libFn.log = () => {}; 340 | } 341 | return function OriginalCaller( 342 | originalEvent, 343 | originalContext, 344 | originalCallback 345 | ) { 346 | const originalIdentity = this; 347 | return new IOpipeWrapperClass( 348 | originalIdentity, 349 | libFn, 350 | plugins, 351 | dnsPromise, 352 | config, 353 | userFunc, 354 | originalEvent, 355 | originalContext, 356 | originalCallback 357 | ).invoke(); 358 | }; 359 | }; 360 | 361 | // Alias decorate to the wrapper function 362 | libFn.decorate = libFn; 363 | return libFn; 364 | }; 365 | 366 | module.exports.getContext = getInvocationContext; 367 | 368 | module.exports.util = { 369 | getFileUploadMeta 370 | }; 371 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import mockContext from 'aws-lambda-mock-context'; 3 | 4 | import { resetEnv } from '../util/testUtils'; 5 | import { SUPPORTED_REGIONS } from './constants'; 6 | 7 | const iopipeLib = require('../dist/iopipe.js'); 8 | 9 | function defaultCatch(err) { 10 | /*eslint-disable no-console*/ 11 | console.error(err); 12 | /*eslint-enable no-console*/ 13 | throw err; 14 | } 15 | 16 | function createContext(opts = {}) { 17 | return mockContext( 18 | Object.assign(opts, { 19 | functionName: 'iopipe-lib-unit-tests' 20 | }) 21 | ); 22 | } 23 | 24 | function createAgent(kwargs) { 25 | return iopipeLib( 26 | _.defaults(kwargs, { 27 | token: 'testSuite' 28 | }) 29 | ); 30 | } 31 | 32 | function runWrappedFunction( 33 | ctx = createContext(), 34 | event = {}, 35 | iopipe = createAgent(), 36 | functionArg 37 | ) { 38 | const defaultFn = (fnEvent, context) => { 39 | context.succeed('Success'); 40 | }; 41 | const fnToRun = functionArg || iopipe(defaultFn); 42 | return new Promise(resolve => { 43 | // not sure why eslint thinks that userFnReturnValue is not reassigned. 44 | /*eslint-disable prefer-const*/ 45 | let userFnReturnValue; 46 | /*eslint-enable prefer-const*/ 47 | function fnResolver(error, response) { 48 | return resolve({ 49 | ctx, 50 | response, 51 | iopipe, 52 | error, 53 | userFnReturnValue 54 | }); 55 | } 56 | userFnReturnValue = fnToRun(event, ctx, fnResolver); 57 | ctx.Promise.then(success => fnResolver(null, success)).catch(fnResolver); 58 | }); 59 | } 60 | 61 | function sendToRegionTest(region = 'us-east-1', done) { 62 | process.env.AWS_REGION = region; 63 | runWrappedFunction(createContext({ region }), undefined, createAgent()).then( 64 | obj => { 65 | expect(obj.response).toEqual('Success'); 66 | expect(obj.error).toBeNull(); 67 | done(); 68 | } 69 | ); 70 | } 71 | 72 | beforeEach(() => { 73 | resetEnv(); 74 | }); 75 | 76 | describe('metrics agent', () => { 77 | test('should return a function', () => { 78 | const agent = createAgent(); 79 | expect(typeof agent).toEqual('function'); 80 | }); 81 | 82 | test('should successfully getRemainingTimeInMillis from aws context', () => { 83 | runWrappedFunction().then(obj => { 84 | expect(typeof obj.ctx.getRemainingTimeInMillis).toBe('function'); 85 | }); 86 | }); 87 | 88 | test('runs the user function and returns the original value', done => { 89 | const iopipe = createAgent(); 90 | const wrappedFunction = iopipe((event, ctx) => { 91 | ctx.succeed('Decorate'); 92 | return 'wow'; 93 | }); 94 | 95 | runWrappedFunction(undefined, undefined, undefined, wrappedFunction) 96 | .then(obj => { 97 | expect(obj.userFnReturnValue).toEqual('wow'); 98 | done(); 99 | }) 100 | .catch(defaultCatch); 101 | }); 102 | 103 | test('allows per-setup configuration', done => { 104 | const completed = { 105 | f1: false, 106 | f2: false 107 | }; 108 | 109 | function fnGenerator(token, region, timeout) { 110 | const iopipe = createAgent({ 111 | token, 112 | url: `https://metrics-api.${region}.iopipe.com` 113 | }); 114 | return iopipe(function Wrapper(event, ctx) { 115 | setTimeout(() => { 116 | completed[token] = true; 117 | ctx.succeed(ctx.iopipe); 118 | }, timeout); 119 | }); 120 | } 121 | 122 | const wrappedFunction1 = fnGenerator('f1', 'us-west-1', 10); 123 | const wrappedFunction2 = fnGenerator('f2', 'us-west-2', 5); 124 | 125 | expect(completed.f1).toBe(false); 126 | expect(completed.f2).toBe(false); 127 | 128 | Promise.all( 129 | [wrappedFunction1, wrappedFunction2].map(fn => 130 | runWrappedFunction(undefined, undefined, undefined, fn) 131 | ) 132 | ) 133 | .then(values => { 134 | const [fn1, fn2] = values; 135 | expect(completed.f1 && completed.f2).toBe(true); 136 | expect(fn1.response.config.clientId).toBe('f1'); 137 | expect(fn2.response.config.clientId).toBe('f2'); 138 | done(); 139 | }) 140 | .catch(defaultCatch); 141 | }); 142 | 143 | test('allows .decorate API', done => { 144 | const iopipe = createAgent(); 145 | const wrappedFunction = iopipe.decorate((event, ctx) => { 146 | ctx.succeed('Decorate'); 147 | }); 148 | 149 | runWrappedFunction(undefined, undefined, undefined, wrappedFunction) 150 | .then(obj => { 151 | expect(obj.response).toEqual('Decorate'); 152 | done(); 153 | }) 154 | .catch(defaultCatch); 155 | }); 156 | 157 | test('Returns a value from context.succeed', done => { 158 | const iopipe = createAgent({ debug: true }); 159 | const wrappedFunction = iopipe((event, ctx) => { 160 | ctx.succeed('my-val'); 161 | }); 162 | 163 | let val; 164 | 165 | wrappedFunction( 166 | {}, 167 | { 168 | succeed: data => { 169 | val = data; 170 | }, 171 | fail: _.noop, 172 | done: _.noop 173 | }, 174 | _.noop 175 | ); 176 | setTimeout(() => { 177 | expect(val).toEqual('my-val'); 178 | done(); 179 | // 1000 is a magic number until the collector uses a mock for testing 180 | }, 1000); 181 | }); 182 | 183 | test('Returns a value from callback', done => { 184 | const iopipe = createAgent({ debug: true }); 185 | const wrappedFunction = iopipe((event, ctx, cb) => { 186 | cb(null, 'my-val'); 187 | }); 188 | 189 | let val; 190 | 191 | wrappedFunction( 192 | {}, 193 | { 194 | succeed: _.noop, 195 | fail: _.noop, 196 | done: _.noop 197 | }, 198 | (err, data) => { 199 | if (err) { 200 | throw err; 201 | } 202 | val = data; 203 | } 204 | ); 205 | setTimeout(() => { 206 | expect(val).toEqual('my-val'); 207 | done(); 208 | // 1000 is a magic number until the collector uses a mock for testing 209 | }, 1000); 210 | }); 211 | 212 | test('has a proper context object', done => { 213 | expect.assertions(6); 214 | const iopipe = createAgent(); 215 | const wrappedFunction = iopipe.decorate((event, ctx) => { 216 | // use json, otherwise it seems circular refs are doing bad things 217 | ctx.callbackWaitsForEmptyEventLoop = true; 218 | ctx.succeed(JSON.stringify(ctx)); 219 | }); 220 | 221 | const testContext = createContext(); 222 | expect(testContext.callbackWaitsForEmptyEventLoop).toBe(true); 223 | testContext.callbackWaitsForEmptyEventLoop = false; 224 | expect(testContext.callbackWaitsForEmptyEventLoop).toBe(false); 225 | 226 | runWrappedFunction(testContext, undefined, undefined, wrappedFunction) 227 | .then(obj => { 228 | const ctx = JSON.parse(obj.response); 229 | expect(_.isObject(ctx)).toBeTruthy(); 230 | expect(ctx.memoryLimitInMB).toBe('128'); 231 | expect(ctx.callbackWaitsForEmptyEventLoop).toBe(true); 232 | expect(testContext.callbackWaitsForEmptyEventLoop).toBe(true); 233 | done(); 234 | }) 235 | .catch(defaultCatch); 236 | }); 237 | 238 | test('will return unwrapped function if token unset', () => { 239 | const fn = (event, context) => { 240 | context.succeed('Success'); 241 | }; 242 | 243 | const agent = createAgent({ token: '' }); 244 | 245 | expect(agent(fn)).toBe(fn); 246 | }); 247 | 248 | test('will return unwrapped function if agent disabled', () => { 249 | const fn = (event, context) => { 250 | context.succeed('Success'); 251 | }; 252 | 253 | const agent = createAgent({ enabled: false }); 254 | 255 | expect(agent(fn)).toBe(fn); 256 | }); 257 | }); 258 | 259 | describe('smoke test', () => { 260 | test('will run when installed on a successful function', done => { 261 | runWrappedFunction().then(obj => { 262 | expect(obj.response).toBeTruthy(); 263 | done(); 264 | }); 265 | }); 266 | 267 | test('will run when installed on a failing function', done => { 268 | const fn = (event, context) => { 269 | context.fail('Whoops!'); 270 | }; 271 | runWrappedFunction(undefined, undefined, undefined, fn).then(obj => { 272 | expect(obj.error instanceof Error).toEqual(true); 273 | expect(obj.error.message).toEqual('Whoops!'); 274 | expect(obj.response).toBeUndefined(); 275 | done(); 276 | }); 277 | }); 278 | 279 | describe('functions using callbacks', () => { 280 | test('will run when installed on a successful function using callbacks', done => { 281 | const fn = (event, ctx, cb) => { 282 | cb(null, 'Success!'); 283 | }; 284 | runWrappedFunction(undefined, undefined, undefined, fn).then(obj => { 285 | expect(obj.response).toEqual('Success!'); 286 | done(); 287 | }); 288 | }); 289 | }); 290 | 291 | describe('sends to specified regions', () => { 292 | for (const region of SUPPORTED_REGIONS.keys()) { 293 | test(`sends to ${region}`, done => { 294 | sendToRegionTest(region, done); 295 | }); 296 | } 297 | }); 298 | }); 299 | -------------------------------------------------------------------------------- /src/invocationContext.js: -------------------------------------------------------------------------------- 1 | let invocationContext; 2 | 3 | export const set = val => (invocationContext = val); 4 | export const get = () => invocationContext; 5 | -------------------------------------------------------------------------------- /src/mockSystem.js: -------------------------------------------------------------------------------- 1 | import uuid from './uuidv4'; 2 | 3 | function readstat() { 4 | return new Promise(resolve => { 5 | setTimeout(() => { 6 | resolve({ 7 | utime: 0, 8 | stime: 0, 9 | cutime: 0, 10 | cstime: 0, 11 | rss: 0 12 | }); 13 | }, 2); 14 | }); 15 | } 16 | function readstatus() { 17 | const mem = process.memoryUsage(); 18 | return Promise.resolve({ 19 | FDSize: 0, 20 | Threads: 1, 21 | VmRSS: mem.rss / 1024 22 | }); 23 | } 24 | 25 | function readbootid() { 26 | return Promise.resolve(uuid()); 27 | } 28 | 29 | export { readstat, readstatus, readbootid }; 30 | -------------------------------------------------------------------------------- /src/mockSystem.test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { readstat, readstatus, readbootid } from './mockSystem'; 3 | 4 | describe('mock system functions', () => { 5 | test('promisifies system data', () => { 6 | const stat = readstat(); 7 | const status = readstatus(); 8 | const bootId = readbootid(); 9 | expect(Promise.resolve(stat)).toEqual(stat); 10 | expect(Promise.resolve(status)).toEqual(stat); 11 | expect(Promise.resolve(bootId)).toEqual(stat); 12 | }); 13 | 14 | test('gives simple 0s for readstat', done => { 15 | expect.assertions(5); 16 | readstat().then(data => { 17 | const { utime, stime, cutime, cstime, rss } = data; 18 | expect(utime).toBe(0); 19 | expect(stime).toBe(0); 20 | expect(cutime).toBe(0); 21 | expect(cstime).toBe(0); 22 | expect(rss).toBe(0); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/plugins.test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import mockContext from 'aws-lambda-mock-context'; 3 | 4 | import { reports } from './sendReport'; 5 | import { hooks } from './hooks'; 6 | 7 | import mockPlugin from './plugins/mock'; 8 | import { 9 | instantiate as allHooksPlugin, 10 | data as allHooksData 11 | } from './plugins/allHooks'; 12 | 13 | jest.mock('./sendReport'); 14 | 15 | const iopipeLib = require('./index'); 16 | 17 | test('Hooks have not changed', () => { 18 | expect(hooks).toHaveLength(6); 19 | expect(hooks).toMatchSnapshot(); 20 | }); 21 | 22 | test('Can instantiate a test plugin', done => { 23 | const plugin = mockPlugin(); 24 | 25 | const invocationInstance = {}; 26 | const pluginInstance = plugin(invocationInstance); 27 | 28 | expect(pluginInstance.hasSetup).toEqual(false); 29 | 30 | done(); 31 | }); 32 | 33 | test('Can instantiate a test plugin with config', done => { 34 | const plugin = mockPlugin({ 35 | foo: 'bar' 36 | }); 37 | 38 | const invocationInstance = {}; 39 | const pluginInstance = plugin(invocationInstance); 40 | 41 | expect(pluginInstance.config.foo).toEqual('bar'); 42 | 43 | done(); 44 | }); 45 | 46 | test('Can call a plugin hook function', done => { 47 | const plugin = mockPlugin(); 48 | 49 | const invocationInstance = { 50 | context: { 51 | iopipe: {} 52 | } 53 | }; 54 | const pluginInstance = plugin(invocationInstance); 55 | 56 | expect(pluginInstance.hasSetup).toBe(false); 57 | pluginInstance.hooks['post:setup'](invocationInstance); 58 | expect(pluginInstance.hasSetup).toBe(true); 59 | 60 | done(); 61 | }); 62 | 63 | test('Can run a test plugin hook that modifies a invocation instance', done => { 64 | const plugin = mockPlugin(); 65 | 66 | const invocationInstance = { context: { iopipe: { log: _.noop } } }; 67 | const pluginInstance = plugin(invocationInstance); 68 | 69 | expect(_.isFunction(invocationInstance.context.iopipe.mock)).toBe(false); 70 | pluginInstance.hooks['post:setup'](); 71 | expect(pluginInstance.hasSetup).toEqual(true); 72 | expect(_.isFunction(invocationInstance.context.iopipe.mock)).toBe(true); 73 | 74 | done(); 75 | }); 76 | 77 | test('Can run a test plugin hook directly', async done => { 78 | const plugin = mockPlugin(); 79 | 80 | const invocationInstance = { 81 | metrics: [ 82 | { 83 | name: 'ding', 84 | s: 'dong' 85 | } 86 | ], 87 | context: { 88 | iopipe: {} 89 | }, 90 | report: { 91 | report: {} 92 | } 93 | }; 94 | const pluginInstance = plugin(invocationInstance); 95 | 96 | pluginInstance.hooks['post:setup'](); 97 | invocationInstance.context.iopipe.mock('metric-2', 'baz'); 98 | await pluginInstance.hooks['pre:invoke'](); 99 | await pluginInstance.hooks['post:invoke'](); 100 | const { metrics } = invocationInstance; 101 | expect(metrics).toHaveLength(2); 102 | expect( 103 | _.find(metrics, m => m.name === 'ding' && m.s === 'dong') 104 | ).toBeTruthy(); 105 | expect( 106 | _.find(metrics, m => m.name === 'mock-metric-2' && m.s === 'baz') 107 | ).toBeTruthy(); 108 | expect( 109 | _.get(invocationInstance, 'report.report.asyncHookFired') 110 | ).toBeTruthy(); 111 | expect( 112 | _.get(invocationInstance, 'report.report.promiseHookFired') 113 | ).toBeTruthy(); 114 | 115 | done(); 116 | }); 117 | 118 | test('A single plugin can be loaded and work', async () => { 119 | try { 120 | const iopipe = iopipeLib({ 121 | token: 'single-plugin', 122 | plugins: [mockPlugin()] 123 | }); 124 | 125 | const wrapped = iopipe((event, ctx) => { 126 | ctx.iopipe.mock('ok', 'neat'); 127 | ctx.succeed(ctx.iopipe.mock); 128 | }); 129 | 130 | const context = mockContext(); 131 | 132 | wrapped({}, context); 133 | 134 | const val = await context.Promise; 135 | expect(_.isFunction(val)).toBe(true); 136 | 137 | const metric = _.chain(reports) 138 | .find(obj => obj.client_id === 'single-plugin') 139 | .get('custom_metrics') 140 | .find({ name: 'mock-ok', s: 'neat' }) 141 | .value(); 142 | expect(_.isObject(metric)).toBe(true); 143 | 144 | const asyncHookFired = _.chain(reports) 145 | .find(obj => obj.client_id === 'single-plugin') 146 | .get('asyncHookFired') 147 | .value(); 148 | expect(asyncHookFired).toBeTruthy(); 149 | 150 | const plugin = _.chain(reports) 151 | .find(obj => obj.client_id === 'single-plugin') 152 | .get('plugins') 153 | .find({ 154 | name: 'mock', 155 | version: '0.0.1', 156 | homepage: 'https://github.com/not/a/real/plugin' 157 | }) 158 | .value(); 159 | 160 | expect(_.isObject(plugin)).toBe(true); 161 | } catch (err) { 162 | throw err; 163 | } 164 | }); 165 | 166 | test('Multiple plugins can be loaded and work', async () => { 167 | try { 168 | const iopipe = iopipeLib({ 169 | token: 'multiple-plugins', 170 | plugins: [ 171 | mockPlugin(), 172 | mockPlugin({ 173 | name: 'secondMockPlugin', 174 | functionName: 'secondmock' 175 | }) 176 | ] 177 | }); 178 | 179 | const wrapped = iopipe((event, ctx) => { 180 | ctx.iopipe.mock('ok', 'neat'); 181 | ctx.iopipe.secondmock('foo', 'bar'); 182 | ctx.succeed('indeed'); 183 | }); 184 | 185 | const context = mockContext(); 186 | 187 | wrapped({}, context); 188 | 189 | const val = await context.Promise; 190 | expect(val).toBe('indeed'); 191 | 192 | const metrics = _.chain(reports) 193 | .find(obj => obj.client_id === 'multiple-plugins') 194 | .get('custom_metrics') 195 | .value(); 196 | expect(_.isArray(metrics)).toBe(true); 197 | expect(metrics).toHaveLength(2); 198 | expect(metrics).toMatchSnapshot(); 199 | } catch (err) { 200 | throw err; 201 | } 202 | }); 203 | 204 | test('All hooks are called successfully when a plugin uses them all', async () => { 205 | try { 206 | const iopipe = iopipeLib({ 207 | token: 'single-plugin', 208 | plugins: [allHooksPlugin()] 209 | }); 210 | 211 | const wrapped = iopipe((event, ctx) => { 212 | ctx.succeed(ctx); 213 | }); 214 | 215 | const context = mockContext(); 216 | 217 | wrapped({}, context); 218 | 219 | const val = await context.Promise; 220 | _.reject(hooks, h => h === 'pre:setup').map(hook => { 221 | return expect(val[`hasRun:${hook}`]).toBe(true); 222 | }); 223 | expect(allHooksData).toMatchSnapshot(); 224 | } catch (err) { 225 | throw err; 226 | } 227 | }); 228 | -------------------------------------------------------------------------------- /src/plugins/allHooks.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { hooks } from '../hooks'; 3 | 4 | const data = []; 5 | 6 | class AllHooksPlugin { 7 | constructor(pluginConfig = {}, invocationInstance) { 8 | this.invocationInstance = invocationInstance; 9 | this.config = _.defaults({}, pluginConfig, { 10 | functionName: 'allHooks' 11 | }); 12 | this.hasSetup = false; 13 | this.hooks = _.chain(hooks) 14 | .map(hook => { 15 | return [hook, this.runHook.bind(this, hook)]; 16 | }) 17 | .fromPairs() 18 | .value(); 19 | return this; 20 | } 21 | get meta() { 22 | return { 23 | name: 'allHooks', 24 | version: '0.0.1', 25 | homepage: 'https://github.com/not/a/real/plugin', 26 | enabled: false 27 | }; 28 | } 29 | runHook(hook) { 30 | const str = `context.hasRun:${hook}`; 31 | _.set(this.invocationInstance, str, true); 32 | data.push(str); 33 | } 34 | } 35 | 36 | function instantiate(pluginOpts) { 37 | return invocationInstance => { 38 | return new AllHooksPlugin(pluginOpts, invocationInstance); 39 | }; 40 | } 41 | 42 | export { data, instantiate }; 43 | -------------------------------------------------------------------------------- /src/plugins/mock.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import delay from 'delay'; 3 | 4 | class MockPlugin { 5 | constructor(pluginConfig = {}, invocationInstance) { 6 | this.invocationInstance = invocationInstance; 7 | this.config = _.defaults({}, pluginConfig, { 8 | functionName: 'mock', 9 | name: 'mock' 10 | }); 11 | this.hasSetup = false; 12 | this.hooks = { 13 | 'post:setup': this.postSetup.bind(this), 14 | 'pre:invoke': this.asyncHook.bind(this), 15 | 'post:invoke': this.promiseHook.bind(this) 16 | }; 17 | return this; 18 | } 19 | get meta() { 20 | return { 21 | name: this.config.name, 22 | version: '0.0.1', 23 | homepage: 'https://github.com/not/a/real/plugin', 24 | enabled: true 25 | }; 26 | } 27 | postSetup() { 28 | this.hasSetup = true; 29 | this.invocationInstance.context.iopipe[ 30 | this.config.functionName 31 | ] = this.mock.bind(this); 32 | return this.config; 33 | } 34 | async asyncHook() { 35 | const { report } = this.invocationInstance.report; 36 | await delay(200); 37 | report.asyncHookFired = true; 38 | } 39 | promiseHook() { 40 | return new Promise(resolve => { 41 | setTimeout(() => { 42 | this.invocationInstance.report.report.promiseHookFired = true; 43 | resolve(); 44 | }, 200); 45 | }); 46 | } 47 | mock(name, s) { 48 | const { metrics = [] } = this.invocationInstance; 49 | metrics.push({ 50 | name: `mock-${name}`, 51 | s 52 | }); 53 | } 54 | } 55 | 56 | export default function instantiate(pluginOpts) { 57 | return invocationInstance => { 58 | return new MockPlugin(pluginOpts, invocationInstance); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/report.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import { sendReport } from './sendReport'; 4 | import strToBool from './util/strToBool'; 5 | import uuid from './uuidv4'; 6 | 7 | import { 8 | COLDSTART, 9 | MODULE_LOAD_TIME, 10 | PROCESS_ID, 11 | VERSION, 12 | setColdStart 13 | } from './globals'; 14 | 15 | const system = 16 | process.platform === 'linux' 17 | ? require('./system.js') 18 | : require('./mockSystem.js'); 19 | 20 | const { log } = console; 21 | 22 | class Report { 23 | constructor(wrapperInstance = {}) { 24 | this.initalPromises = { 25 | statPromise: system.readstat('self'), 26 | bootIdPromise: system.readbootid() 27 | }; 28 | 29 | // flag on report preparation status, reports are prepared once 30 | this.prepared = false; 31 | 32 | // flag on report sending status, reports are sent once 33 | this.sent = false; 34 | 35 | const { 36 | config = {}, 37 | context = {}, 38 | dnsPromise = Promise.resolve(), 39 | metrics = [], 40 | labels = new Set(), 41 | plugins = [], 42 | startTime = process.hrtime(), 43 | startTimestamp = Date.now() 44 | } = wrapperInstance; 45 | 46 | this.config = config; 47 | this.context = context; 48 | this.startTime = startTime; 49 | this.dnsPromise = dnsPromise; 50 | 51 | // Populate initial report skeleton on construction 52 | const { 53 | functionName, 54 | functionVersion, 55 | logGroupName, 56 | logStreamName, 57 | memoryLimitInMB 58 | } = this.context; 59 | 60 | let { invokedFunctionArn, awsRequestId } = this.context; 61 | 62 | // Patch invokedFunctionArn in cases of SAM local invocations 63 | // or if invokedFunctionArn is missing 64 | if (invokedFunctionArn === undefined || process.env.AWS_SAM_LOCAL) { 65 | invokedFunctionArn = `arn:aws:lambda:local:0:function:${functionName}`; 66 | } 67 | 68 | if ( 69 | process.env.AWS_SAM_LOCAL || 70 | awsRequestId === undefined || 71 | awsRequestId === 'id' 72 | ) { 73 | awsRequestId = `local|${uuid()}`; 74 | } 75 | 76 | const pluginMetas = plugins 77 | .filter(plugin => typeof plugin !== 'undefined') 78 | .map(plugin => { 79 | const meta = plugin.meta || {}; 80 | 81 | if (meta) { 82 | meta.uploads = meta.uploads || []; 83 | if (meta.enabled && typeof meta.enabled === 'number') { 84 | meta.enabled = Boolean(meta.enabled); 85 | } 86 | 87 | if (meta.enabled && typeof meta.enabled === 'string') { 88 | meta.enabled = strToBool(meta.enabled); 89 | } 90 | } 91 | 92 | return meta; 93 | }); 94 | 95 | this.report = { 96 | /*eslint-disable camelcase*/ 97 | client_id: this.config.clientId || undefined, 98 | installMethod: this.config.installMethod, 99 | duration: undefined, 100 | processId: PROCESS_ID, 101 | timestamp: startTimestamp, 102 | aws: { 103 | functionName, 104 | functionVersion, 105 | awsRequestId, 106 | invokedFunctionArn, 107 | logGroupName, 108 | logStreamName, 109 | memoryLimitInMB, 110 | getRemainingTimeInMillis: undefined, 111 | /*eslint-disable no-underscore-dangle*/ 112 | traceId: process.env._X_AMZN_TRACE_ID 113 | /*eslint-enable no-underscore-dangle*/ 114 | }, 115 | environment: { 116 | agent: { 117 | runtime: 'nodejs', 118 | version: VERSION, 119 | load_time: MODULE_LOAD_TIME 120 | }, 121 | runtime: { 122 | name: process.release.name, 123 | version: process.version 124 | }, 125 | nodejs: { 126 | version: process.version, 127 | memoryUsage: undefined 128 | }, 129 | host: { 130 | container_id: undefined 131 | }, 132 | os: {} 133 | }, 134 | errors: {}, 135 | coldstart: COLDSTART, 136 | custom_metrics: metrics, 137 | labels, 138 | plugins: pluginMetas 139 | }; 140 | 141 | // Set to false after coldstart 142 | setColdStart(false); 143 | } 144 | 145 | async prepare(err) { 146 | // Prepare report only once 147 | if (this.prepared) { 148 | return; 149 | } 150 | 151 | this.prepared = true; 152 | 153 | const config = this.config; 154 | const context = this.context; 155 | 156 | // Add error to report if necessary 157 | if (err) { 158 | this.report.labels.add('@iopipe/error'); 159 | const reportError = 160 | err instanceof Error 161 | ? err 162 | : new Error(typeof err === 'string' ? err : JSON.stringify(err)); 163 | const { 164 | name, 165 | message, 166 | stack, 167 | lineNumber, 168 | columnNumber, 169 | fileName 170 | } = reportError; 171 | 172 | this.report.errors = { 173 | name, 174 | message, 175 | stack, 176 | lineNumber, 177 | columnNumber, 178 | fileName 179 | }; 180 | } 181 | 182 | // Resolve system promises/report data 183 | const results = await Promise.all([ 184 | this.initalPromises.statPromise, 185 | system.readstat('self'), 186 | system.readstatus('self'), 187 | this.initalPromises.bootIdPromise 188 | ]); 189 | 190 | const [preProcSelfStat, procSelfStat, procSelfStatus, bootId] = results; 191 | 192 | const totalmem = os.totalmem(); 193 | const freemem = os.freemem(); 194 | 195 | const osStats = { 196 | hostname: os.hostname(), 197 | uptime: os.uptime(), 198 | totalmem, 199 | freemem, 200 | usedmem: totalmem - freemem, 201 | cpus: os.cpus(), 202 | arch: os.arch(), 203 | linux: { 204 | pid: { 205 | self: { 206 | stat_start: preProcSelfStat, 207 | stat: procSelfStat, 208 | status: procSelfStatus 209 | } 210 | } 211 | } 212 | }; 213 | 214 | this.report.environment.os = osStats; 215 | this.report.environment.host.boot_id = bootId; 216 | this.report.environment.nodejs.memoryUsage = process.memoryUsage(); 217 | 218 | if (context.getRemainingTimeInMillis) { 219 | this.report.aws.getRemainingTimeInMillis = context.getRemainingTimeInMillis(); 220 | } 221 | 222 | this.report.timestampEnd = Date.now(); 223 | 224 | const durationHrTime = process.hrtime(this.startTime); 225 | 226 | this.report.duration = Math.ceil( 227 | durationHrTime[0] * 1e9 + durationHrTime[1] 228 | ); 229 | 230 | if (this.report.coldstart) { 231 | this.report.labels.add('@iopipe/coldstart'); 232 | } 233 | 234 | // Convert labels from set to array 235 | this.report.labels = Array.from(this.report.labels); 236 | 237 | if (config.debug) { 238 | log('IOPIPE-DEBUG: ', JSON.stringify(this.report)); 239 | } 240 | /*eslint-enable camelcase*/ 241 | } 242 | 243 | send(callback) { 244 | // Send report only once 245 | if (this.sent) { 246 | return; 247 | } 248 | 249 | this.sent = true; 250 | 251 | const self = this; 252 | const config = this.config; 253 | 254 | this.dnsPromise 255 | .then(ipAddress => { 256 | sendReport(self.report, config, ipAddress) 257 | .then(function afterRequest(res) { 258 | if (config.debug) { 259 | log(`API STATUS FROM ${config.host}: ${res.status}`); 260 | log(`API RESPONSE FROM ${config.host}: ${res.apiResponse}`); 261 | } 262 | 263 | callback(null, res); 264 | }) 265 | .catch(function handleErr(err) { 266 | // Log errors, don't block on failed requests 267 | if (config.debug) { 268 | log('Write to IOpipe failed'); 269 | log(err); 270 | } 271 | 272 | callback(err); 273 | }); 274 | }) 275 | .catch(err => { 276 | // Log errors, don't block on failed requests 277 | if (config.debug) { 278 | log('Write to IOpipe failed. DNS resolution error.'); 279 | log(err); 280 | } 281 | 282 | callback(err); 283 | }); 284 | } 285 | } 286 | 287 | export default Report; 288 | -------------------------------------------------------------------------------- /src/report.test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import flatten from 'flat'; 3 | import context from 'aws-lambda-mock-context'; 4 | 5 | import { resetEnv } from '../util/testUtils'; 6 | import Report from './report'; 7 | import mockPlugin from './plugins/mock'; 8 | 9 | const schema = require('./schema.json'); 10 | 11 | const config = { 12 | clientId: 'foo' 13 | }; 14 | 15 | beforeEach(() => { 16 | resetEnv(); 17 | }); 18 | 19 | describe('Report creation', () => { 20 | test('creates a new report object', () => { 21 | expect( 22 | typeof new Report({ 23 | config, 24 | context: context() 25 | }) 26 | ).toBe('object'); 27 | }); 28 | 29 | test('can take no arguments', () => { 30 | expect(typeof new Report()).toBe('object'); 31 | }); 32 | 33 | test('creates a report that matches the schema, including httpTraceEntries and dbTraceEntries', async done => { 34 | const r = new Report({ 35 | labels: new Set('a-label'), 36 | metrics: [{ name: 'foo-metric', s: 'wow-string', n: 99 }] 37 | }); 38 | await r.prepare(new Error('Holy smokes!')); 39 | r.send(() => { 40 | const flatReport = _.chain(r.report) 41 | .thru(flatten) 42 | .keys() 43 | .value(); 44 | const flatSchema = _.chain(schema) 45 | .thru(flatten) 46 | .keys() 47 | .value(); 48 | const diff = _.difference(flatSchema, flatReport); 49 | 50 | const allowedMissingFields = [ 51 | 'clientId', 52 | 'projectId', 53 | 'disk.totalMiB', 54 | 'disk.usedMiB', 55 | 'disk.usedPercentage', 56 | 'memory.rssMiB', 57 | 'memory.totalMiB', 58 | 'memory.rssTotalPercentage', 59 | 'environment.runtime.vendor', 60 | 'environment.runtime.vmVersion', 61 | 'environment.runtime.vmVendor', 62 | 'environment.python.version', 63 | 'errors.stackHash', 64 | 'errors.count', 65 | 'eventType', 66 | 'performanceEntries.0.name', 67 | 'performanceEntries.0.startTime', 68 | 'performanceEntries.0.duration', 69 | 'performanceEntries.0.entryType', 70 | 'performanceEntries.0.timestamp', 71 | 'plugins.0.name', 72 | 'plugins.0.version', 73 | 'plugins.0.homepage', 74 | 'plugins.0.enabled', 75 | 'plugins.0.uploads.0', 76 | 'httpTraceEntries.0.startTime', 77 | 'httpTraceEntries.0.name', 78 | 'httpTraceEntries.0.duration', 79 | 'httpTraceEntries.0.timestamp', 80 | 'httpTraceEntries.0.type', 81 | 'httpTraceEntries.0.request.hash', 82 | 'httpTraceEntries.0.request.hostname', 83 | 'httpTraceEntries.0.request.method', 84 | 'httpTraceEntries.0.request.path', 85 | 'httpTraceEntries.0.request.pathname', 86 | 'httpTraceEntries.0.request.port', 87 | 'httpTraceEntries.0.request.protocol', 88 | 'httpTraceEntries.0.request.query', 89 | 'httpTraceEntries.0.request.url', 90 | 'httpTraceEntries.0.request.headers.0.key', 91 | 'httpTraceEntries.0.request.headers.0.string', 92 | 'httpTraceEntries.0.request.headers.1.key', 93 | 'httpTraceEntries.0.request.headers.1.string', 94 | 'httpTraceEntries.0.response.headers.0.key', 95 | 'httpTraceEntries.0.response.headers.0.string', 96 | 'httpTraceEntries.0.response.headers.1.key', 97 | 'httpTraceEntries.0.response.headers.1.number', 98 | 'httpTraceEntries.0.response.statusCode', 99 | 'httpTraceEntries.0.response.statusMessage', 100 | 'httpTraceEntries.0.hasResponse', 101 | 'dbTraceEntries.0.startTime', 102 | 'dbTraceEntries.0.duration', 103 | 'dbTraceEntries.0.timestamp', 104 | 'dbTraceEntries.0.dbType', 105 | 'dbTraceEntries.0.request.command', 106 | 'dbTraceEntries.0.request.bulkCommands', 107 | 'dbTraceEntries.0.request.key', 108 | 'dbTraceEntries.0.request.hostname', 109 | 'dbTraceEntries.0.request.port', 110 | 'dbTraceEntries.0.request.connectionName', 111 | 'dbTraceEntries.0.request.db', 112 | 'dbTraceEntries.0.request.table', 113 | 'dbTraceEntries.0.type' 114 | ]; 115 | 116 | expect(_.isEqual(allowedMissingFields, diff)).toBe(true); 117 | 118 | done(); 119 | }); 120 | }); 121 | 122 | test('keeps custom metrics references', () => { 123 | const myMetrics = []; 124 | const r = new Report({ config, context: context(), metrics: myMetrics }); 125 | myMetrics.push({ n: 1, name: 'a_value' }); 126 | 127 | expect(r.report.custom_metrics).toHaveLength(1); 128 | }); 129 | 130 | test('tracks plugins in use', () => { 131 | const plugin = mockPlugin(); 132 | 133 | const r = new Report({ plugins: [plugin()] }); 134 | 135 | expect(r.report.plugins).toHaveLength(1); 136 | 137 | expect(r.report.plugins[0].name).toBe('mock'); 138 | 139 | expect(r.report.plugins[0].version).toBe('0.0.1'); 140 | 141 | expect(r.report.plugins[0].homepage).toBe( 142 | 'https://github.com/not/a/real/plugin' 143 | ); 144 | }); 145 | 146 | test('patches the ARN if SAM local is detected', () => { 147 | process.env.AWS_SAM_LOCAL = true; 148 | const localReport = new Report({ config, context: context() }); 149 | expect(localReport.report.aws.invokedFunctionArn).toBe( 150 | 'arn:aws:lambda:local:0:function:aws-lambda-mock-context' 151 | ); 152 | 153 | delete process.env.AWS_SAM_LOCAL; 154 | const normalReport = new Report({ config, context: context() }); 155 | expect(normalReport.report.aws.invokedFunctionArn).toBe( 156 | 'arn:aws:lambda:us-west-1:123456789012:function:aws-lambda-mock-context:$LATEST' 157 | ); 158 | }); 159 | 160 | test('patches the ARN if no ARN is detected', () => { 161 | const newContext = context(); 162 | delete newContext.invokedFunctionArn; 163 | const localReport = new Report({ config, context: newContext }); 164 | expect(localReport.report.aws.invokedFunctionArn).toBe( 165 | 'arn:aws:lambda:local:0:function:aws-lambda-mock-context' 166 | ); 167 | }); 168 | 169 | test('patches the awsRequestId if SAM local is detected', () => { 170 | process.env.AWS_SAM_LOCAL = true; 171 | const localReport = new Report({ config, context: context() }); 172 | const requestId = String(localReport.report.aws.awsRequestId); 173 | 174 | expect(requestId.substring(0, 6)).toBe(`local|`); 175 | expect(requestId).toHaveLength(42); 176 | 177 | delete process.env.AWS_SAM_LOCAL; 178 | const normalReport = new Report({ config, context: context() }); 179 | expect(normalReport.report.aws.invokedFunctionArn).toBe( 180 | 'arn:aws:lambda:us-west-1:123456789012:function:aws-lambda-mock-context:$LATEST' 181 | ); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /src/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id": "s", 3 | "clientId": "s", 4 | "projectId": "s", 5 | "installMethod": "s", 6 | "duration": "i", 7 | "processId": "s", 8 | "timestamp": "n", 9 | "timestampEnd": "n", 10 | "aws": { 11 | "functionName": "s", 12 | "functionVersion": "s", 13 | "awsRequestId": "s", 14 | "invokedFunctionArn": "s", 15 | "logGroupName": "s", 16 | "logStreamName": "s", 17 | "memoryLimitInMB": "n", 18 | "getRemainingTimeInMillis": "n", 19 | "traceId": "s" 20 | }, 21 | "disk": { 22 | "totalMiB": "n", 23 | "usedMiB": "n", 24 | "usedPercentage": "n" 25 | }, 26 | "memory": { 27 | "rssMiB": "n", 28 | "totalMiB": "n", 29 | "rssTotalPercentage": "n" 30 | }, 31 | "environment": { 32 | "agent": { 33 | "runtime": "s", 34 | "version": "s", 35 | "load_time": "n" 36 | }, 37 | "runtime": { 38 | "name": "s", 39 | "version": "s", 40 | "vendor": "s", 41 | "vmVersion": "s", 42 | "vmVendor": "s" 43 | }, 44 | "nodejs": { 45 | "version": "s", 46 | "memoryUsage": { 47 | "rss": "n" 48 | } 49 | }, 50 | "python": { 51 | "version": "s" 52 | }, 53 | "host": { 54 | "boot_id": "s" 55 | }, 56 | "os": { 57 | "hostname": "s", 58 | "totalmem": "n", 59 | "freemem": "n", 60 | "usedmem": "n", 61 | "cpus": [ 62 | { 63 | "times": { 64 | "idle": "n", 65 | "irq": "n", 66 | "sys": "n", 67 | "user": "n", 68 | "nice": "n" 69 | } 70 | } 71 | ], 72 | "linux": { 73 | "pid": { 74 | "self": { 75 | "stat": { 76 | "utime": "n", 77 | "stime": "n", 78 | "cutime": "n", 79 | "cstime": "n" 80 | }, 81 | "stat_start": { 82 | "utime": "n", 83 | "stime": "n", 84 | "cutime": "n", 85 | "cstime": "n" 86 | }, 87 | "status": { 88 | "VmRSS": "n", 89 | "Threads": "n", 90 | "FDSize": "n" 91 | } 92 | } 93 | } 94 | } 95 | } 96 | }, 97 | "errors": { 98 | "stack": "s", 99 | "name": "s", 100 | "message": "s", 101 | "stackHash": "s", 102 | "count": "n" 103 | }, 104 | "coldstart": "b", 105 | "custom_metrics": [ 106 | { 107 | "name": "s", 108 | "s": "s", 109 | "n": "n" 110 | } 111 | ], 112 | "eventType": "s", 113 | "labels": ["s"], 114 | "performanceEntries": [ 115 | { 116 | "name": "s", 117 | "startTime": "n", 118 | "duration": "n", 119 | "entryType": "s", 120 | "timestamp": "n" 121 | } 122 | ], 123 | "plugins": [ 124 | { 125 | "name": "s", 126 | "version": "s", 127 | "homepage": "s", 128 | "enabled": "b", 129 | "uploads": ["s"] 130 | } 131 | ], 132 | "httpTraceEntries": [ 133 | { 134 | "startTime": "n", 135 | "name": "s", 136 | "duration": "n", 137 | "timestamp": "n", 138 | "type": "s", 139 | "request": { 140 | "hash": "s", 141 | "hostname": "s", 142 | "method": "s", 143 | "path": "s", 144 | "pathname": "s", 145 | "port": "n", 146 | "protocol": "s", 147 | "query": "s", 148 | "url": "s", 149 | "headers": [ 150 | { 151 | "key": "s", 152 | "string": "s" 153 | }, 154 | { 155 | "key": "s", 156 | "string": "s" 157 | } 158 | ] 159 | }, 160 | "response": { 161 | "headers": [ 162 | { 163 | "key": "s", 164 | "string": "s" 165 | }, 166 | { 167 | "key": "s", 168 | "number": "n" 169 | } 170 | ], 171 | "statusCode": "n", 172 | "statusMessage": "s" 173 | }, 174 | "hasResponse": "b" 175 | } 176 | ], 177 | "dbTraceEntries": [ 178 | { 179 | "startTime": "n", 180 | "duration": "n", 181 | "timestamp": "n", 182 | "dbType": "s", 183 | "request": { 184 | "command": "s", 185 | "bulkCommands": "s", 186 | "key": "s", 187 | "hostname": "s", 188 | "port": "s", 189 | "connectionName": "s", 190 | "db": "s", 191 | "table": "s" 192 | }, 193 | "type": "s" 194 | } 195 | ] 196 | } 197 | -------------------------------------------------------------------------------- /src/sendReport.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable import/prefer-default-export*/ 2 | import https from 'https'; 3 | 4 | import { httpsAgent } from './globals'; 5 | 6 | const reports = undefined; 7 | 8 | function sendReport(requestBody, config, ipAddress) { 9 | return new Promise((resolve, reject) => { 10 | const req = https 11 | .request( 12 | { 13 | hostname: ipAddress, 14 | servername: config.host, 15 | path: config.path, 16 | port: 443, 17 | method: 'POST', 18 | headers: { 19 | authorization: `Bearer ${config.clientId}`, 20 | 'content-type': 'application/json' 21 | }, 22 | agent: httpsAgent 23 | }, 24 | res => { 25 | let apiResponse = ''; 26 | 27 | res.on('data', chunk => { 28 | apiResponse += chunk; 29 | }); 30 | 31 | res.on('end', () => { 32 | resolve({ status: res.statusCode, apiResponse }); 33 | }); 34 | } 35 | ) 36 | .on('error', err => { 37 | reject(err); 38 | }); 39 | if (Number.isInteger(config.networkTimeout) && config.networkTimeout > 0) { 40 | req.setTimeout(config.networkTimeout, () => { 41 | req.abort(); 42 | }); 43 | } 44 | 45 | req.write(JSON.stringify(requestBody)); 46 | req.end(); 47 | }); 48 | } 49 | 50 | export { reports, sendReport }; 51 | -------------------------------------------------------------------------------- /src/system.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs'; 2 | 3 | function readstat(pid) { 4 | return new Promise((resolve, reject) => { 5 | readFile(`/proc/${pid}/stat`, function handleRead(err, statFile) { 6 | const preProcSelfStatFields = (statFile || '').toString().split(' '); 7 | if (err) return reject(err); 8 | return resolve({ 9 | utime: preProcSelfStatFields[13], 10 | stime: preProcSelfStatFields[14], 11 | cutime: preProcSelfStatFields[15], 12 | cstime: preProcSelfStatFields[16], 13 | rss: preProcSelfStatFields[23] 14 | }); 15 | }); 16 | }); 17 | } 18 | 19 | function readstatus(pid) { 20 | return new Promise((resolve, reject) => { 21 | readFile(`/proc/${pid}/status`, function handleRead(err, data) { 22 | const procSelfStatusFields = {}; 23 | if (err) return reject(err); 24 | // Parse status file and apply to the procSelfStatusFields dict. 25 | data 26 | .toString() 27 | .split('\n') 28 | .map(line => { 29 | return line ? line.split('\t') : [null, null]; 30 | }) 31 | .forEach(field => { 32 | const [key, value] = field; 33 | if (key && value) { 34 | const trimmedKey = key.replace(':', ''); 35 | const cleanValue = Number(value.replace(/\D/g, '')); 36 | procSelfStatusFields[trimmedKey] = cleanValue; 37 | } 38 | }); 39 | return resolve({ 40 | FDSize: procSelfStatusFields.FDSize, 41 | Threads: procSelfStatusFields.Threads, 42 | VmRSS: procSelfStatusFields.VmRSS 43 | }); 44 | }); 45 | }); 46 | } 47 | 48 | function readbootid() { 49 | return new Promise((resolve, reject) => { 50 | readFile('/proc/sys/kernel/random/boot_id', function handleRead(err, data) { 51 | return err ? reject(err) : resolve(data.toString()); 52 | }); 53 | }); 54 | } 55 | 56 | module.exports = { 57 | readstat, 58 | readstatus, 59 | readbootid 60 | }; 61 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | export function convertToString(value) { 2 | if (typeof value === 'object') { 3 | return JSON.stringify(value); 4 | } 5 | return String(value); 6 | } 7 | -------------------------------------------------------------------------------- /src/util.test.js: -------------------------------------------------------------------------------- 1 | import { convertToString } from './util'; 2 | 3 | test('converts to string', () => { 4 | expect(convertToString('Foo')).toBe('Foo'); 5 | expect(convertToString(undefined)).toBe('undefined'); 6 | expect(convertToString(NaN)).toBe('NaN'); 7 | expect(convertToString({ foo: 1 })).toBe('{"foo":1}'); 8 | expect(convertToString([1, 2])).toBe('[1,2]'); 9 | expect(convertToString(true)).toBe('true'); 10 | expect(convertToString(100)).toBe('100'); 11 | expect(convertToString(null)).toBe('null'); 12 | expect(convertToString(Symbol('foo'))).toBe('Symbol(foo)'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/util/setupPlugins.js: -------------------------------------------------------------------------------- 1 | import uniqBy from 'lodash.uniqby'; 2 | 3 | const defaultPluginFunction = () => { 4 | return {}; 5 | }; 6 | 7 | export default function setupPlugins(identity, plugins) { 8 | let pluginCounter = 0; 9 | return uniqBy( 10 | plugins.map((pluginFn = defaultPluginFunction) => { 11 | if (typeof pluginFn === 'function') { 12 | return pluginFn(identity); 13 | } 14 | return {}; 15 | }), 16 | plugin => { 17 | return (plugin && plugin.meta && plugin.meta.name) || pluginCounter++; 18 | } 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/util/strToBool.js: -------------------------------------------------------------------------------- 1 | export default function(string) { 2 | switch (string.toLowerCase().trim()) { 3 | case 'true': 4 | case 't': 5 | case '1': 6 | return true; 7 | case 'false': 8 | case 'f': 9 | case '0': 10 | case null: 11 | return false; 12 | default: 13 | return Boolean(string); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/util/strToBool.test.js: -------------------------------------------------------------------------------- 1 | import strToBool from './strToBool'; 2 | 3 | test('Strings are cast as bools', () => { 4 | expect(strToBool('true')).toBe(true); 5 | expect(strToBool('True')).toBe(true); 6 | expect(strToBool('t')).toBe(true); 7 | expect(strToBool('1')).toBe(true); 8 | 9 | expect(strToBool('false')).toBe(false); 10 | expect(strToBool('False')).toBe(false); 11 | expect(strToBool('f')).toBe(false); 12 | expect(strToBool('0')).toBe(false); 13 | }); 14 | -------------------------------------------------------------------------------- /src/uuidv4.js: -------------------------------------------------------------------------------- 1 | /* https://gist.github.com/jed/982883 */ 2 | /*eslint-disable no-bitwise*/ 3 | import crypto from 'crypto'; 4 | 5 | function uuid(a) { 6 | return a 7 | ? (a ^ ((crypto.randomBytes(1)[0] % 16) >> (a / 4))).toString(16) 8 | : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); 9 | } 10 | 11 | export default uuid; 12 | -------------------------------------------------------------------------------- /src/uuidv4.test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import uuid from './uuidv4'; 3 | 4 | const numResults = 1e4; 5 | 6 | test('Generates random uuid', () => { 7 | const results = _.range(numResults).map(() => uuid()); 8 | expect(_.uniq(results)).toHaveLength(numResults); 9 | }); 10 | -------------------------------------------------------------------------------- /testProjects/catchTypeError/handler.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-unreachable*/ 2 | 3 | throw new TypeError('wow-great-type-error'); 4 | 5 | module.exports = (event, context) => { 6 | context.succeed(); 7 | }; 8 | -------------------------------------------------------------------------------- /testProjects/catchTypeError/handler.test.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable import/no-extraneous-dependencies*/ 2 | import _ from 'lodash'; 3 | import delay from 'delay'; 4 | 5 | import { resetEnv } from '../../util/testUtils'; 6 | 7 | const iopipe = require('./iopipe'); 8 | 9 | beforeEach(() => { 10 | resetEnv(); 11 | }); 12 | 13 | describe('Catch typeError by wrapping require block', () => { 14 | test('Has configuration', async () => { 15 | let inspectableInvocation; 16 | const result = await new Promise(resolve => { 17 | return iopipe({ 18 | token: 'test-token', 19 | plugins: [ 20 | inv => { 21 | inspectableInvocation = inv; 22 | } 23 | ] 24 | })((event, context) => require('./handler')(event, context))( 25 | {}, 26 | {}, 27 | resolve 28 | ); 29 | }); 30 | await delay(100); 31 | expect(_.isError(result)).toBe(true); 32 | expect(result.message).toMatch('wow-great-type-error'); 33 | const { 34 | report: { report: { errors: { message } } } 35 | } = inspectableInvocation; 36 | expect(message).toMatch('wow-great-type-error'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /testProjects/catchTypeError/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catchTypeErrorTest", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.test.js", 6 | "scripts": { 7 | "test": "node ../../node_modules/jest/bin/jest.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /testProjects/catchTypeError/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /testProjects/extendConfig/handler.test.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable import/no-extraneous-dependencies*/ 2 | import _ from 'lodash'; 3 | 4 | import { MockPlugin } from '../util/plugins'; 5 | import { resetEnv } from '../../util/testUtils'; 6 | 7 | const iopipe = require('./iopipe'); 8 | 9 | beforeEach(() => { 10 | resetEnv(); 11 | }); 12 | 13 | describe('Using extend iopipe configuration', () => { 14 | test('Has configuration', done => { 15 | let inspectableInvocation; 16 | iopipe({ 17 | extends: '@iopipe/config', 18 | clientId: 'foobar', 19 | plugins: [ 20 | inv => { 21 | inspectableInvocation = inv; 22 | return new MockPlugin(inv); 23 | } 24 | ] 25 | })((event, context) => { 26 | try { 27 | const { config } = context.iopipe; 28 | const { plugins } = inspectableInvocation; 29 | 30 | expect(config.extends).toBe('@iopipe/config'); 31 | 32 | expect(plugins).toHaveLength(2); 33 | 34 | const names = _.map(plugins, 'meta.name'); 35 | expect(names).toEqual(['mock-plugin', '@iopipe/trace']); 36 | 37 | expect(_.isFunction(plugins[1].hooks['post:setup'])).toBe(true); 38 | 39 | expect(_.isFunction(context.iopipe.mark.start)).toBe(true); 40 | 41 | done(); 42 | } catch (err) { 43 | throw err; 44 | } 45 | })({}, {}); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /testProjects/extendConfig/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extendConfigTest", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@iopipe/config": { 8 | "version": "0.1.0", 9 | "resolved": "https://registry.npmjs.org/@iopipe/config/-/config-0.1.0.tgz", 10 | "integrity": "sha1-LDdkBdYv9GxXdfBT7mWr6qa9MUE=" 11 | }, 12 | "@iopipe/trace": { 13 | "version": "0.3.0", 14 | "resolved": "https://registry.npmjs.org/@iopipe/trace/-/trace-0.3.0.tgz", 15 | "integrity": "sha1-QqVY2px9MQ0V8J/dtJD7t+u785k=", 16 | "requires": { 17 | "iopipe": "1.4.0", 18 | "performance-node": "0.2.0" 19 | } 20 | }, 21 | "argparse": { 22 | "version": "1.0.9", 23 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", 24 | "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", 25 | "requires": { 26 | "sprintf-js": "1.0.3" 27 | } 28 | }, 29 | "cosmiconfig": { 30 | "version": "3.1.0", 31 | "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz", 32 | "integrity": "sha512-zedsBhLSbPBms+kE7AH4vHg6JsKDz6epSv2/+5XHs8ILHlgDciSJfSWf8sX9aQ52Jb7KI7VswUTsLpR/G0cr2Q==", 33 | "requires": { 34 | "is-directory": "0.3.1", 35 | "js-yaml": "3.10.0", 36 | "parse-json": "3.0.0", 37 | "require-from-string": "2.0.1" 38 | } 39 | }, 40 | "error-ex": { 41 | "version": "1.3.1", 42 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", 43 | "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", 44 | "requires": { 45 | "is-arrayish": "0.2.1" 46 | } 47 | }, 48 | "esprima": { 49 | "version": "4.0.0", 50 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", 51 | "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" 52 | }, 53 | "iopipe": { 54 | "version": "1.4.0", 55 | "resolved": "https://registry.npmjs.org/iopipe/-/iopipe-1.4.0.tgz", 56 | "integrity": "sha1-f6H2E0/isx7ttYhM3Tuj9I+TpMU=", 57 | "requires": { 58 | "cosmiconfig": "3.1.0" 59 | } 60 | }, 61 | "is-arrayish": { 62 | "version": "0.2.1", 63 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 64 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" 65 | }, 66 | "is-directory": { 67 | "version": "0.3.1", 68 | "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", 69 | "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" 70 | }, 71 | "js-yaml": { 72 | "version": "3.10.0", 73 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", 74 | "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", 75 | "requires": { 76 | "argparse": "1.0.9", 77 | "esprima": "4.0.0" 78 | } 79 | }, 80 | "parse-json": { 81 | "version": "3.0.0", 82 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-3.0.0.tgz", 83 | "integrity": "sha1-+m9HsY4jgm6tMvJj50TQ4ehH+xM=", 84 | "requires": { 85 | "error-ex": "1.3.1" 86 | } 87 | }, 88 | "performance-node": { 89 | "version": "0.2.0", 90 | "resolved": "https://registry.npmjs.org/performance-node/-/performance-node-0.2.0.tgz", 91 | "integrity": "sha1-A6v3SsDEVfMzvlBRZdrKuREO9bM=" 92 | }, 93 | "require-from-string": { 94 | "version": "2.0.1", 95 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", 96 | "integrity": "sha1-xUUjPp19pmFunVmt+zn8n1iGdv8=" 97 | }, 98 | "sprintf-js": { 99 | "version": "1.0.3", 100 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 101 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /testProjects/extendConfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extendConfigTest", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.test.js", 6 | "scripts": { 7 | "test": "node ../../node_modules/jest/bin/jest.js" 8 | }, 9 | "dependencies": { 10 | "@iopipe/config": "^0.1.0", 11 | "@iopipe/trace": "^0.3.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testProjects/extendConfig/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@iopipe/config@^0.1.0": 6 | version "0.1.0" 7 | resolved "https://registry.yarnpkg.com/@iopipe/config/-/config-0.1.0.tgz#2c376405d62ff46c5775f053ee65abeaa6bd3141" 8 | 9 | "@iopipe/trace@^0.3.0": 10 | version "0.3.0" 11 | resolved "https://registry.yarnpkg.com/@iopipe/trace/-/trace-0.3.0.tgz#42a558da9c7d310d15f09fddb490fbb7ebbbf399" 12 | dependencies: 13 | iopipe "^1.0.0" 14 | performance-node "^0.2.0" 15 | 16 | argparse@^1.0.7: 17 | version "1.0.9" 18 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" 19 | dependencies: 20 | sprintf-js "~1.0.2" 21 | 22 | cosmiconfig@^3.1.0: 23 | version "3.1.0" 24 | resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" 25 | dependencies: 26 | is-directory "^0.3.1" 27 | js-yaml "^3.9.0" 28 | parse-json "^3.0.0" 29 | require-from-string "^2.0.1" 30 | 31 | error-ex@^1.3.1: 32 | version "1.3.1" 33 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" 34 | dependencies: 35 | is-arrayish "^0.2.1" 36 | 37 | esprima@^4.0.0: 38 | version "4.0.0" 39 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" 40 | 41 | iopipe@^1.0.0: 42 | version "1.4.0" 43 | resolved "https://registry.yarnpkg.com/iopipe/-/iopipe-1.4.0.tgz#7fa1f6134fe2b31eedb5884cdd3ba3f48f93a4c5" 44 | dependencies: 45 | cosmiconfig "^3.1.0" 46 | 47 | is-arrayish@^0.2.1: 48 | version "0.2.1" 49 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 50 | 51 | is-directory@^0.3.1: 52 | version "0.3.1" 53 | resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" 54 | 55 | js-yaml@^3.9.0: 56 | version "3.10.0" 57 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" 58 | dependencies: 59 | argparse "^1.0.7" 60 | esprima "^4.0.0" 61 | 62 | parse-json@^3.0.0: 63 | version "3.0.0" 64 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" 65 | dependencies: 66 | error-ex "^1.3.1" 67 | 68 | performance-node@^0.2.0: 69 | version "0.2.0" 70 | resolved "https://registry.yarnpkg.com/performance-node/-/performance-node-0.2.0.tgz#03abf74ac0c455f333be505165dacab9110ef5b3" 71 | 72 | require-from-string@^2.0.1: 73 | version "2.0.1" 74 | resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" 75 | 76 | sprintf-js@~1.0.2: 77 | version "1.0.3" 78 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 79 | -------------------------------------------------------------------------------- /testProjects/metaWithExtraPlugins/handler.test.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable import/no-extraneous-dependencies*/ 2 | import _ from 'lodash'; 3 | 4 | import { MockPlugin, MockTracePlugin } from '../util/plugins'; 5 | import { resetEnv } from '../../util/testUtils'; 6 | 7 | const iopipe = require('./iopipe'); 8 | 9 | beforeEach(() => { 10 | resetEnv(); 11 | }); 12 | 13 | describe('Meta with extra plugin, no deduping', () => { 14 | test('Has configuration', done => { 15 | let inspectableInvocation; 16 | iopipe({ 17 | clientId: 'foobar', 18 | plugins: [ 19 | inv => { 20 | inspectableInvocation = inv; 21 | return new MockPlugin(inv); 22 | } 23 | ] 24 | })((event, context) => { 25 | try { 26 | const { config } = context.iopipe; 27 | const { plugins } = inspectableInvocation; 28 | 29 | expect(config.extends).toEqual({ plugins: ['@iopipe/trace'] }); 30 | 31 | const names = _.chain(plugins) 32 | .map(p => p.meta.name) 33 | .value(); 34 | 35 | expect(plugins).toHaveLength(2); 36 | expect(names).toEqual(['mock-plugin', '@iopipe/trace']); 37 | 38 | expect(_.isFunction(context.iopipe.mark.start)).toBe(true); 39 | 40 | done(); 41 | } catch (err) { 42 | throw err; 43 | } 44 | })({}, {}); 45 | }); 46 | }); 47 | 48 | describe('Meta with extra plugin, dedupes trace plugin', () => { 49 | /* When a consumer provides their own plugins, the plugins should be deduped via the meta.name string. If a consumer provides a duplicate with the same meta.name, their plugin should be used instead of the default. */ 50 | 51 | test('Has configuration', done => { 52 | let inspectableInvocation; 53 | iopipe({ 54 | clientId: 'foobar', 55 | plugins: [ 56 | inv => { 57 | inspectableInvocation = inv; 58 | return new MockPlugin(inv); 59 | }, 60 | inv => new MockTracePlugin(inv) 61 | ] 62 | })((event, context) => { 63 | try { 64 | const { config } = context.iopipe; 65 | const { plugins } = inspectableInvocation; 66 | 67 | expect(config.extends).toEqual({ plugins: ['@iopipe/trace'] }); 68 | 69 | const names = _.chain(plugins) 70 | .map(p => p.meta.name) 71 | .value(); 72 | 73 | expect(plugins).toHaveLength(2); 74 | expect(names).toEqual(['mock-plugin', '@iopipe/trace']); 75 | expect(plugins[1].meta.version).toBe('mocked-trace'); 76 | 77 | done(); 78 | } catch (err) { 79 | throw err; 80 | } 81 | })({}, {}); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /testProjects/metaWithExtraPlugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extendConfigTest", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.test.js", 6 | "scripts": { 7 | "test": "node ../../node_modules/jest/bin/jest.js" 8 | }, 9 | "dependencies": { 10 | "@iopipe/config": "^0.3.0", 11 | "@iopipe/trace": "^0.3.0" 12 | }, 13 | "iopipe": { 14 | "extends": "@iopipe/config" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testProjects/metaWithExtraPlugins/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@iopipe/config@^0.3.0": 6 | version "0.3.0" 7 | resolved "https://registry.yarnpkg.com/@iopipe/config/-/config-0.3.0.tgz#80e16e26dd51cb3f857a80feba35af3fd4c72601" 8 | dependencies: 9 | "@iopipe/trace" "^0.3.0" 10 | 11 | "@iopipe/trace@^0.3.0": 12 | version "0.3.0" 13 | resolved "https://registry.yarnpkg.com/@iopipe/trace/-/trace-0.3.0.tgz#42a558da9c7d310d15f09fddb490fbb7ebbbf399" 14 | dependencies: 15 | iopipe "^1.0.0" 16 | performance-node "^0.2.0" 17 | 18 | argparse@^1.0.7: 19 | version "1.0.10" 20 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" 21 | dependencies: 22 | sprintf-js "~1.0.2" 23 | 24 | cosmiconfig@^3.1.0: 25 | version "3.1.0" 26 | resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" 27 | dependencies: 28 | is-directory "^0.3.1" 29 | js-yaml "^3.9.0" 30 | parse-json "^3.0.0" 31 | require-from-string "^2.0.1" 32 | 33 | error-ex@^1.3.1: 34 | version "1.3.1" 35 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" 36 | dependencies: 37 | is-arrayish "^0.2.1" 38 | 39 | esprima@^4.0.0: 40 | version "4.0.1" 41 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 42 | 43 | iopipe@^1.0.0: 44 | version "1.4.0" 45 | resolved "https://registry.yarnpkg.com/iopipe/-/iopipe-1.4.0.tgz#7fa1f6134fe2b31eedb5884cdd3ba3f48f93a4c5" 46 | dependencies: 47 | cosmiconfig "^3.1.0" 48 | 49 | is-arrayish@^0.2.1: 50 | version "0.2.1" 51 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 52 | 53 | is-directory@^0.3.1: 54 | version "0.3.1" 55 | resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" 56 | 57 | js-yaml@^3.9.0: 58 | version "3.13.1" 59 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" 60 | dependencies: 61 | argparse "^1.0.7" 62 | esprima "^4.0.0" 63 | 64 | parse-json@^3.0.0: 65 | version "3.0.0" 66 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" 67 | dependencies: 68 | error-ex "^1.3.1" 69 | 70 | performance-node@^0.2.0: 71 | version "0.2.0" 72 | resolved "https://registry.yarnpkg.com/performance-node/-/performance-node-0.2.0.tgz#03abf74ac0c455f333be505165dacab9110ef5b3" 73 | 74 | require-from-string@^2.0.1: 75 | version "2.0.1" 76 | resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" 77 | 78 | sprintf-js@~1.0.2: 79 | version "1.0.3" 80 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 81 | -------------------------------------------------------------------------------- /testProjects/networkTimeout/handler.test.js: -------------------------------------------------------------------------------- 1 | const iopipe = require('./iopipe.js'); 2 | 3 | describe('Call to non-existant endpoint should time out after specd timeout - 100ms', () => { 4 | test('Test network timeouts', done => { 5 | const getMeta = iopipe.util.getFileUploadMeta; 6 | const start = new Date().getTime(); 7 | iopipe({ 8 | clientId: 'foobar' 9 | })(async () => { 10 | try { 11 | await getMeta({ 12 | requestId: 'testFoo', 13 | networkTimeout: 100, 14 | url: 'http://example.com:81' 15 | }); 16 | } catch (e) { 17 | const end = new Date().getTime(); 18 | const duration = end - start; 19 | expect(e.message).toEqual('Request timed out'); 20 | expect(duration).toBeGreaterThan(100); 21 | expect(duration).toBeLessThan(1000); 22 | done(); 23 | } 24 | })({}, {}); 25 | }); 26 | }); 27 | 28 | describe('Call to non-existant endpoint should time out after specd timeout - 3500ms', () => { 29 | test('Test network timeouts', done => { 30 | const getMeta = iopipe.util.getFileUploadMeta; 31 | const start = new Date().getTime(); 32 | iopipe({ 33 | clientId: 'foobar' 34 | })(async () => { 35 | try { 36 | await getMeta({ 37 | requestId: 'testFoo', 38 | networkTimeout: 3500, 39 | url: 'http://example.com:81' 40 | }); 41 | } catch (e) { 42 | const end = new Date().getTime(); 43 | const duration = end - start; 44 | expect(e.message).toEqual('Request timed out'); 45 | expect(duration).toBeGreaterThan(3500); 46 | expect(duration).toBeLessThan(4500); 47 | done(); 48 | } 49 | })({}, {}); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /testProjects/networkTimeout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catchTypeErrorTest", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.test.js", 6 | "scripts": { 7 | "test": "node ../../node_modules/jest/bin/jest.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /testProjects/networkTimeout/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /testProjects/packageJsonConfig/handler.test.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable import/no-extraneous-dependencies*/ 2 | import _ from 'lodash'; 3 | 4 | import { MockPlugin } from '../util/plugins'; 5 | import { resetEnv } from '../../util/testUtils'; 6 | 7 | const iopipe = require('./iopipe'); 8 | 9 | beforeEach(() => { 10 | resetEnv(); 11 | }); 12 | 13 | describe('Using package.json iopipe configuration', () => { 14 | test('Has configuration', done => { 15 | let inspectableInvocation; 16 | iopipe({ 17 | networkTimeout: 345, 18 | plugins: [ 19 | inv => { 20 | inspectableInvocation = inv; 21 | return new MockPlugin(inv); 22 | } 23 | ] 24 | })((event, context) => { 25 | try { 26 | const { clientId, networkTimeout } = context.iopipe.config; 27 | const { plugins } = inspectableInvocation; 28 | 29 | expect(clientId).toBe('package_json_config_token_wow'); 30 | expect(networkTimeout).toBe(345); 31 | 32 | expect(_.map(plugins, 'meta.name')).toEqual([ 33 | 'mock-plugin', 34 | '@iopipe/trace' 35 | ]); 36 | 37 | expect(_.isFunction(context.iopipe.mark.start)).toBe(true); 38 | // the config should be "empty"... 39 | expect(_.isEmpty(context.iopipe.config)).toBe(true); 40 | // ...but we can get at them like this: 41 | const configs = _.chain( 42 | Object.getOwnPropertySymbols(context.iopipe.config) 43 | ) 44 | .map(o => [o.toString(), context.iopipe.config[o]]) 45 | .fromPairs() 46 | .value(); 47 | expect(configs['Symbol(cosmi)'].token).toBe( 48 | 'package_json_config_token_wow' 49 | ); 50 | done(); 51 | } catch (err) { 52 | throw err; 53 | } 54 | })({}, {}); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /testProjects/packageJsonConfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packageJsonConfigTest", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.test.js", 6 | "scripts": { 7 | "test": "node ../../node_modules/jest/bin/jest.js" 8 | }, 9 | "iopipe": { 10 | "token": "package_json_config_token_wow", 11 | "plugins": [ 12 | "@iopipe/trace" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testProjects/packageJsonConfig/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /testProjects/promiseRejection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promiseRejectionTest", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.test.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "dependencies": { 10 | "aws-lambda-mock-context": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testProjects/promiseRejection/test.js: -------------------------------------------------------------------------------- 1 | const mockContext = require('aws-lambda-mock-context'); 2 | const iopipe = require('./iopipe'); 3 | /*eslint-disable no-process-exit*/ 4 | /*eslint-disable no-console*/ 5 | 6 | const invocations = []; 7 | 8 | const handler = iopipe({ 9 | token: 'test', 10 | plugins: [ 11 | inv => { 12 | invocations.push(inv); 13 | } 14 | ] 15 | })((event, context) => { 16 | const prom = new Promise(() => { 17 | throw new Error('An error from a promise'); 18 | }); 19 | prom.then(context.succeed); 20 | }); 21 | 22 | const ctx = mockContext({ timeout: 1 }); 23 | handler({}, ctx); 24 | 25 | process.on('unhandledRejection', () => { 26 | setTimeout(() => { 27 | if ( 28 | invocations[0].report.report.labels.has( 29 | '@iopipe/unhandled-promise-rejection' 30 | ) 31 | ) { 32 | process.exit(0); 33 | } 34 | console.error('Promise Error was NOT caught by iopipe.'); 35 | process.exit(1); 36 | }, 10); 37 | }); 38 | -------------------------------------------------------------------------------- /testProjects/promiseRejection/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | aws-lambda-mock-context@^3.1.1: 6 | version "3.1.1" 7 | resolved "https://registry.yarnpkg.com/aws-lambda-mock-context/-/aws-lambda-mock-context-3.1.1.tgz#29b2d00bf9dc8bc461f749f8585691ee205573d5" 8 | dependencies: 9 | moment "^2.10.5" 10 | pinkie-defer "^1.0.0" 11 | uuid "^3.0.1" 12 | 13 | moment@^2.10.5: 14 | version "2.22.1" 15 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad" 16 | 17 | pinkie-defer@^1.0.0: 18 | version "1.0.0" 19 | resolved "https://registry.yarnpkg.com/pinkie-defer/-/pinkie-defer-1.0.0.tgz#78fcc22116c0da890ac92a808ad370888ded9421" 20 | 21 | uuid@^3.0.1: 22 | version "3.2.1" 23 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" 24 | -------------------------------------------------------------------------------- /testProjects/rcFileConfig/.iopiperc: -------------------------------------------------------------------------------- 1 | { 2 | "token": "rc_file_config_token_wow", 3 | "plugins": [ 4 | "@iopipe/trace" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /testProjects/rcFileConfig/handler.test.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable import/no-extraneous-dependencies*/ 2 | import _ from 'lodash'; 3 | 4 | import { MockPlugin } from '../util/plugins'; 5 | import { resetEnv } from '../../util/testUtils'; 6 | 7 | const iopipe = require('./iopipe'); 8 | 9 | beforeEach(() => { 10 | resetEnv(); 11 | }); 12 | 13 | describe('Using rc file iopipe configuration', () => { 14 | test('Has configuration', done => { 15 | let inspectableInvocation; 16 | iopipe({ 17 | networkTimeout: 345, 18 | plugins: [ 19 | inv => { 20 | inspectableInvocation = inv; 21 | return new MockPlugin(inv); 22 | } 23 | ] 24 | })((event, context) => { 25 | try { 26 | const { clientId, networkTimeout } = context.iopipe.config; 27 | const { plugins } = inspectableInvocation; 28 | 29 | expect(clientId).toBe('rc_file_config_token_wow'); 30 | 31 | expect(networkTimeout).toBe(345); 32 | 33 | expect(_.map(plugins, 'meta.name')).toEqual([ 34 | 'mock-plugin', 35 | '@iopipe/trace' 36 | ]); 37 | 38 | expect(_.isFunction(context.iopipe.mark.start)).toBe(true); 39 | // the config should be "empty"... 40 | expect(_.isEmpty(context.iopipe.config)).toBe(true); 41 | // ...but we can get at them like this: 42 | const configs = _.chain( 43 | Object.getOwnPropertySymbols(context.iopipe.config) 44 | ) 45 | .map(o => [o.toString(), context.iopipe.config[o]]) 46 | .fromPairs() 47 | .value(); 48 | 49 | expect(configs['Symbol(cosmi)'].token).toBe('rc_file_config_token_wow'); 50 | done(); 51 | } catch (err) { 52 | throw err; 53 | } 54 | })({}, {}); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /testProjects/rcFileConfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rcFileConfigTest", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.test.js", 6 | "scripts": { 7 | "test": "node ../../node_modules/jest/bin/jest.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /testProjects/rcFileConfig/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /testProjects/util/plugins.js: -------------------------------------------------------------------------------- 1 | export class MockPlugin { 2 | constructor() { 3 | return this; 4 | } 5 | get meta() { 6 | return { 7 | name: 'mock-plugin' 8 | }; 9 | } 10 | } 11 | 12 | export class MockTracePlugin { 13 | constructor() { 14 | return this; 15 | } 16 | get meta() { 17 | return { 18 | name: '@iopipe/trace', 19 | version: 'mocked-trace' 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /util/testProjects.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console*/ 2 | /*eslint-disable no-process-exit*/ 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const _ = require('lodash'); 6 | const spawn = require('cross-spawn'); 7 | const argv = require('yargs').argv; 8 | 9 | const testDirFiles = fs.readdirSync(path.join(__dirname, '../testProjects')); 10 | const folders = _.chain(testDirFiles) 11 | .reject(s => s.match(/^\./)) 12 | .reject(s => s === 'util') 13 | .filter(file => { 14 | const toInclude = argv.folders || argv.projects; 15 | if (toInclude) { 16 | return _.includes(toInclude, file); 17 | } 18 | return true; 19 | }) 20 | .value(); 21 | 22 | const results = []; 23 | function resultPush({ status }) { 24 | results.push(status); 25 | } 26 | 27 | folders.forEach(folder => { 28 | console.log(`Running tests for ${folder}...`); 29 | resultPush( 30 | spawn.sync('yarn', ['install', '--cwd', `testProjects/${folder}`], { 31 | stdio: 'inherit' 32 | }) 33 | ); 34 | 35 | fs.copyFileSync('dist/iopipe.js', `testProjects/${folder}/iopipe.js`); 36 | 37 | resultPush( 38 | spawn.sync('yarn', ['--cwd', `testProjects/${folder}`, 'test'], { 39 | stdio: 'inherit' 40 | }) 41 | ); 42 | console.log(`Finished tests for ${folder}.`); 43 | }); 44 | 45 | process.exit(_.max(results)); 46 | -------------------------------------------------------------------------------- /util/testUtils.js: -------------------------------------------------------------------------------- 1 | const supportedEnvVars = [ 2 | 'IOPIPE_TOKEN', 3 | 'IOPIPE_DEBUG', 4 | 'IOPIPE_ENABLED', 5 | 'IOPIPE_INSTALL_METHOD', 6 | 'IOPIPE_CLIENTID', 7 | 'IOPIPE_NETWORK_TIMEOUT', 8 | 'IOPIPE_TIMEOUT_WINDOW', 9 | 'AWS_REGION', 10 | 'AWS_SAM_LOCAL' 11 | ]; 12 | 13 | function resetEnv() { 14 | supportedEnvVars.forEach(str => delete process.env[str]); 15 | } 16 | 17 | export { resetEnv, supportedEnvVars }; 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const _ = require('lodash'); 3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 4 | .BundleAnalyzerPlugin; 5 | const nodeExternals = require('webpack-node-externals'); 6 | 7 | module.exports = { 8 | target: 'node', 9 | entry: './src/index', 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | filename: './iopipe.js', 13 | libraryTarget: 'commonjs2', 14 | library: 'iopipe' 15 | }, 16 | plugins: _.compact([process.env.ANALYZE && new BundleAnalyzerPlugin()]), 17 | externals: [ 18 | nodeExternals({ 19 | whitelist: [/babel-runtime/, /regenerator-runtime/, /core-js/] 20 | }) 21 | ], 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | exclude: /(node_modules)/, 27 | use: { 28 | loader: 'babel-loader' 29 | } 30 | } 31 | ] 32 | } 33 | }; 34 | --------------------------------------------------------------------------------