├── .circleci └── config.yml ├── .dir-locals.el ├── .editorconfig ├── .esdoc.json ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── question-discussion.md │ └── security-vulnerability-report.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── release.yml └── workflows │ ├── add-to-project-v2.yml │ ├── apply-labels.yml │ ├── stale.yml │ └── validate-pr-title.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── DEVELOPMENT.md ├── LICENSE ├── Makefile ├── NOTICE ├── OSSMETADATA ├── README.md ├── RELEASE.md ├── SECURITY.md ├── SUPPORT.md ├── babel.config.js ├── docker-compose.yml ├── examples ├── README.md ├── express-dynamic-fields │ ├── .gitignore │ ├── app.js │ ├── express-honey.js │ └── package.json ├── express-response-time │ ├── .gitignore │ ├── app.js │ └── package.json ├── express │ ├── .gitignore │ ├── app.js │ ├── express-honey.js │ └── package.json └── factorial │ ├── FactorialDockerfile │ ├── docker-compose.yml │ ├── factorial.js │ └── package.json ├── package-lock.json ├── package.json ├── rollup.browser.config.js ├── rollup.config.js ├── setupTests.js └── src ├── LICENSE ├── __tests__ ├── builder_test.js ├── event_test.js ├── libhoney_test.js └── transmission_test.js ├── builder.js ├── event.js ├── foreach.js ├── libhoney.js └── transmission.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # YAML Anchors to reduce copypasta 4 | 5 | # This is necessary for job to run when a tag is created 6 | filters_always: &filters_always 7 | filters: 8 | tags: 9 | only: /.*/ 10 | 11 | # Restrict running to only be on tags starting with vNNNN 12 | filters_publish: &filters_publish 13 | filters: 14 | tags: 15 | only: /^v[0-9].*/ 16 | branches: 17 | ignore: /.*/ 18 | 19 | matrix_nodeversions: &matrix_nodeversions 20 | matrix: 21 | parameters: 22 | nodeversion: ["14.20", "16.17", "18.9"] 23 | 24 | # Default version of node to use for lint and publishing 25 | default_nodeversion: &default_nodeversion "16.17" 26 | 27 | executors: 28 | node: 29 | parameters: 30 | nodeversion: 31 | type: string 32 | default: *default_nodeversion 33 | docker: 34 | - image: cimg/node:<< parameters.nodeversion >> 35 | github: 36 | docker: 37 | - image: cibuilds/github:0.13.0 38 | 39 | commands: 40 | publish_github: 41 | steps: 42 | - attach_workspace: 43 | at: ~/ 44 | - run: 45 | name: "Artifacts being published" 46 | command: | 47 | echo "about to publish to tag ${CIRCLE_TAG}" 48 | ls -l ~/artifacts/* 49 | - run: 50 | name: "GHR Draft" 51 | command: ghr -draft -n ${CIRCLE_TAG} -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${CIRCLE_TAG} ~/artifacts 52 | 53 | jobs: 54 | lint: 55 | executor: 56 | name: node 57 | nodeversion: *default_nodeversion 58 | steps: 59 | - checkout 60 | - run: npm ci 61 | - run: npm run lint 62 | 63 | test: 64 | parameters: 65 | nodeversion: 66 | type: string 67 | default: *default_nodeversion 68 | executor: 69 | name: node 70 | nodeversion: "<< parameters.nodeversion >>" 71 | steps: 72 | - checkout 73 | - run: npm ci 74 | - run: npm run test -- --verbose 75 | - run: npm run build 76 | 77 | smoke_test: 78 | machine: 79 | image: ubuntu-2204:2024.01.1 80 | steps: 81 | - checkout 82 | - attach_workspace: 83 | at: ./ 84 | - run: 85 | name: Spin up example app 86 | command: make smoke 87 | - run: 88 | name: Spin down example app 89 | command: make unsmoke 90 | 91 | build_artifacts: 92 | executor: 93 | name: node 94 | steps: 95 | - checkout 96 | - run: mkdir -p ~/artifacts 97 | - run: npm ci 98 | - run: npm run build 99 | - run: cp ./dist/* ~/artifacts/ 100 | - persist_to_workspace: 101 | root: ~/ 102 | paths: 103 | - artifacts 104 | - store_artifacts: 105 | path: ~/artifacts 106 | 107 | publish_github: 108 | executor: github 109 | steps: 110 | - publish_github 111 | 112 | publish_npm: 113 | executor: 114 | name: node 115 | steps: 116 | - checkout 117 | - run: 118 | name: store npm auth token 119 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc 120 | - run: npm ci 121 | - run: npm run build 122 | - run: npm publish 123 | 124 | workflows: 125 | nightly: 126 | triggers: 127 | - schedule: 128 | cron: "0 0 * * *" 129 | filters: 130 | branches: 131 | only: 132 | - main 133 | jobs: 134 | - lint 135 | - test: 136 | requires: 137 | - lint 138 | <<: *matrix_nodeversions 139 | 140 | build: 141 | jobs: 142 | - lint: 143 | <<: *filters_always 144 | - test: 145 | <<: *filters_always 146 | requires: 147 | - lint 148 | <<: *matrix_nodeversions 149 | - build_artifacts: 150 | <<: *filters_always 151 | requires: 152 | - test 153 | - smoke_test: 154 | <<: *filters_always 155 | requires: 156 | - test 157 | - publish_github: 158 | <<: *filters_publish 159 | context: Honeycomb Secrets for Public Repos 160 | requires: 161 | - build_artifacts 162 | - publish_npm: 163 | <<: *filters_publish 164 | context: Honeycomb Secrets for Public Repos 165 | requires: 166 | - test 167 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((fill-column . 100))) 2 | (js2-mode . ((indent-tabs-mode . nil) 3 | (tab-width . 2) 4 | (js2-basic-offset . 2))) 5 | ) 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_size = 2 11 | indent_style = space 12 | 13 | [{Makefile,*.mk}] 14 | indent_style = tab 15 | 16 | [*.md] 17 | indent_size = 4 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "plugins": [ 5 | {"name": "esdoc-standard-plugin"}, 6 | {"name": "esdoc-ecmascript-proposal-plugin", "option": { 7 | "classProperties": true, 8 | "objectRestSpread": true 9 | }} 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true 5 | }, 6 | "extends": ["eslint:recommended"], 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaVersion": 2017, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "no-console": "off", 14 | "semi": 2, 15 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 16 | "prefer-arrow-callback": "error", 17 | "no-var": "error", 18 | "eqeqeq": "error", 19 | "quotes": ["error", "double", { "avoidEscape": true }], 20 | "camelcase": "error", 21 | "sort-imports": "error", 22 | "no-param-reassign": "error", 23 | "no-shadow": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file. 2 | # This file controls who is tagged for review for any given pull request. 3 | 4 | # For anything not explicitly taken by someone else: 5 | * @honeycombio/pipeline-team 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know if something is not working as expected 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | 18 | **Versions** 19 | 20 | - Node: 21 | - Libhoney: 22 | 23 | **Steps to reproduce** 24 | 25 | 1. 26 | 27 | **Additional context** 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'type: enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | **Is your feature request related to a problem? Please describe.** 17 | 18 | 19 | **Describe the solution you'd like** 20 | 21 | 22 | **Describe alternatives you've considered** 23 | 24 | 25 | **Additional context** 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question/Discussion 3 | about: General question about how things work or a discussion 4 | title: '' 5 | labels: 'type: discussion' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/security-vulnerability-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security vulnerability report 3 | about: Let us know if you discover a security vulnerability 4 | title: '' 5 | labels: 'type: security' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | **Versions** 16 | 17 | - Node: 18 | - Libhoney: 19 | 20 | **Description** 21 | 22 | (Please include any relevant CVE advisory links) 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ## Which problem is this PR solving? 14 | 15 | - 16 | 17 | ## Short description of the changes 18 | 19 | - 20 | 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | labels: 13 | - "type: dependencies" 14 | commit-message: 15 | prefix: "maint" 16 | include: "scope" 17 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | 3 | changelog: 4 | exclude: 5 | labels: 6 | - no-changelog 7 | categories: 8 | - title: 💥 Breaking Changes 💥 9 | labels: 10 | - "version: bump major" 11 | - breaking-change 12 | - title: 💡 Enhancements 13 | labels: 14 | - "type: enhancement" 15 | - title: 🐛 Fixes 16 | labels: 17 | - "type: bug" 18 | - title: 🛠 Maintenance 19 | labels: 20 | - "type: maintenance" 21 | - "type: dependencies" 22 | - "type: documentation" 23 | - title: 🤷 Other Changes 24 | labels: 25 | - "*" 26 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project-v2.yml: -------------------------------------------------------------------------------- 1 | name: Add to project 2 | on: 3 | issues: 4 | types: [opened] 5 | pull_request_target: 6 | types: [opened] 7 | jobs: 8 | add-to-project: 9 | runs-on: ubuntu-latest 10 | name: Add issues and PRs to project 11 | steps: 12 | - uses: actions/add-to-project@main 13 | with: 14 | project-url: https://github.com/orgs/honeycombio/projects/27 15 | github-token: ${{ secrets.GHPROJECTS_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/apply-labels.yml: -------------------------------------------------------------------------------- 1 | name: Apply project labels 2 | on: [issues, pull_request_target, label] 3 | jobs: 4 | apply-labels: 5 | runs-on: ubuntu-latest 6 | name: Apply common project labels 7 | steps: 8 | - uses: honeycombio/oss-management-actions/labels@v1 9 | with: 10 | github-token: ${{ secrets.GITHUB_TOKEN }} 11 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | name: 'Close stale issues and PRs' 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | 14 | steps: 15 | - uses: actions/stale@v4 16 | with: 17 | start-date: '2021-09-01T00:00:00Z' 18 | stale-issue-message: 'Marking this issue as stale because it has been open 14 days with no activity. Please add a comment if this is still an ongoing issue; otherwise this issue will be automatically closed in 7 days.' 19 | stale-pr-message: 'Marking this PR as stale because it has been open 30 days with no activity. Please add a comment if this PR is still relevant; otherwise this PR will be automatically closed in 7 days.' 20 | close-issue-message: 'Closing this issue due to inactivity. Please see our [Honeycomb OSS Lifecyle and Practices](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md).' 21 | close-pr-message: 'Closing this PR due to inactivity. Please see our [Honeycomb OSS Lifecyle and Practices](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md).' 22 | days-before-issue-stale: 14 23 | days-before-pr-stale: 30 24 | days-before-issue-close: 7 25 | days-before-pr-close: 7 26 | any-of-labels: 'status: info needed,status: revision needed' 27 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: "Validate PR Title" 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | id: lint_pr_title 17 | name: "🤖 Check PR title follows conventional commit spec" 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | # Have to specify all types because `maint` and `rel` aren't defaults 22 | types: | 23 | maint 24 | rel 25 | fix 26 | feat 27 | chore 28 | ci 29 | docs 30 | style 31 | refactor 32 | perf 33 | test 34 | ignoreLabels: | 35 | "type: dependencies" 36 | # When the previous steps fails, the workflow would stop. By adding this 37 | # condition you can continue the execution with the populated error message. 38 | - if: always() && (steps.lint_pr_title.outputs.error_message != null) 39 | name: "📝 Add PR comment about using conventional commit spec" 40 | uses: marocchino/sticky-pull-request-comment@v2 41 | with: 42 | header: pr-title-lint-error 43 | message: | 44 | Thank you for contributing to the project! 🎉 45 | 46 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 47 | 48 | Make sure to prepend with `feat:`, `fix:`, or another option in the list below. 49 | 50 | Once you update the title, this workflow will re-run automatically and validate the updated title. 51 | 52 | Details: 53 | 54 | ``` 55 | ${{ steps.lint_pr_title.outputs.error_message }} 56 | ``` 57 | 58 | # Delete a previous comment when the issue has been resolved 59 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 60 | name: "❌ Delete PR comment after title has been updated" 61 | uses: marocchino/sticky-pull-request-comment@v2 62 | with: 63 | header: pr-title-lint-error 64 | delete: true 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *~ 5 | yarn-error.log 6 | docs 7 | examples/*/package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | examples/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # libhoney-js changelog 2 | 3 | ## [4.3.1] - 2024-12-20 4 | 5 | ### Maintenance 6 | 7 | - maint: update superagent to pull in updated hexoid (#447) | [@codeboten](https://github.com/codeboten) 8 | - maint(deps-dev): bump @babel/preset-env from 7.24.4 to 7.26.0 (#439) 9 | - maint(deps-dev): bump @rollup/plugin-commonjs from 25.0.7 to 28.0.1 (#440) 10 | - maint(deps-dev): bump braces from 3.0.2 to 3.0.3 (#425) 11 | - maint(deps-dev): bump rollup from 3.20.2 to 3.29.5 (#433) 12 | - maint(deps-dev): bump @babel/core from 7.24.4 to 7.26.0 (#435) 13 | - maint: update dependabot.yml to remove review team, use codeowners instead (#434) | [@codeboten](https://github.com/codeboten) 14 | - maint(deps-dev): bump prettier from 3.0.0 to 3.3.2 (#427) 15 | - docs: update vulnerability reporting process (#431) | [@robbkidd](https://github.com/robbkidd) 16 | 17 | ## [4.3.0] - 2024-04-25 18 | 19 | ### !!! Breaking Changes !!! 20 | 21 | Minimum Node version is now 14.18. 22 | 23 | ### Maintenance 24 | 25 | - maint: Update ubuntu image in workflows to latest (#409) | @MikeGoldsmith 26 | - maint: Add labels to release.yml for auto-generated grouping (#408) | @JamieDanielson 27 | - maint(deps): bump formidable and superagent (#414) | @dependabot 28 | - maint(deps): bump ip from 1.1.8 to 1.1.9 (#413) | @dependabot 29 | - maint(deps-dev): bump @babel/preset-env from 7.22.9 to 7.24.4 (#415) | @dependabot 30 | - maint(deps-dev): bump @babel/traverse from 7.22.8 to 7.24.1 (#412) | @dependabot 31 | - maint(deps-dev): bump @rollup/plugin-node-resolve from 15.1.0 to 15.2.3 (#402) | @dependabot 32 | - maint(deps-dev): bump @rollup/plugin-commonjs from 25.0.3 to 25.0.7 (#401) | @dependabot 33 | - maint(deps-dev): bump babel-jest from 29.6.2 to 29.7.0 (#398) | @dependabot 34 | - maint(deps-dev): bump jest from 29.5.0 to 29.7.0 (#399) | @dependabot 35 | 36 | ## [4.2.0] - 2024-02-28 37 | 38 | ### Enhancements 39 | 40 | - feat: support classic ingest keys (#406) | [@cewkrupa](https://github.com/cewkrupa) 41 | 42 | ### Maintenance 43 | 44 | - maint: update codeowners to pipeline-team (#405) | [@JamieDanielson](https://github.com/JamieDanielson) 45 | - maint: update codeowners to pipeline (#404) | [@JamieDanielson](https://github.com/JamieDanielson) 46 | 47 | ## [4.1.0] - 2023-08-17 48 | 49 | ### Fixes 50 | 51 | - fix: replace superagent-proxy with direct use of proxy-agent (#389) | [@robbkidd](https://github.com/robbkidd) 52 | - Note: This resolves a security vulnerability in transitive dependency vm2 (CVE-2023-37466) 53 | 54 | ### Maintenance 55 | 56 | - maint: extra test to check for http info from proxy (#390) | [@JamieDanielson](https://github.com/JamieDanielson) 57 | - docs: add development.md (#374) | [@vreynolds](https://github.com/vreynolds) 58 | - maint: add smoke test (#383) | [@vreynolds](https://github.com/vreynolds) 59 | - maint(deps-dev): bump eslint from 8.24.0 to 8.46.0 (#387) 60 | - maint(deps-dev): bump @rollup/plugin-commonjs from 25.0.2 to 25.0.3 (#385) 61 | - maint(deps-dev): bump @babel/preset-env from 7.22.5 to 7.22.9 (#386) 62 | - maint(deps-dev): bump babel-jest from 29.5.0 to 29.6.2 (#384) 63 | - maint(deps-dev): bump prettier from 2.8.7 to 3.0.0 (#388) 64 | - maint(deps-dev): bump @babel/core from 7.20.12 to 7.22.9 (#382) 65 | - maint(deps-dev): bump @rollup/plugin-node-resolve from 15.0.1 to 15.1.0 (#376) 66 | - maint(deps-dev): bump @rollup/plugin-commonjs from 24.0.1 to 25.0.2 (#377) 67 | - maint(deps-dev): bump @babel/preset-env from 7.21.4 to 7.22.5 (#378) 68 | - maint(deps): bump word-wrap from 1.2.3 to 1.2.4 (#381) 69 | - maint(deps): bump superagent from 8.0.2 to 8.0.9 (#365) 70 | - maint(deps): bump semver from 6.3.0 to 6.3.1 (#379) 71 | - maint(deps-dev): bump @babel/eslint-parser from 7.19.1 to 7.21.8 (#370) 72 | - maint(deps): bump vm2 from 3.9.17 to 3.9.18 (#369) 73 | - maint(deps-dev): bump jest from 29.1.2 to 29.5.0 (#366) 74 | - maint(deps-dev): bump rollup from 2.79.0 to 3.20.2 (#358) 75 | - maint(deps-dev): bump @babel/preset-env from 7.19.3 to 7.21.4 (#357) 76 | - maint(deps): bump vm2 from 3.9.16 to 3.9.17 (#361) 77 | - maint(deps-dev): bump prettier from 2.7.1 to 2.8.7 (#355) 78 | - maint(deps-dev): bump babel-jest from 29.3.1 to 29.5.0 (#356) 79 | - maint(deps-dev): bump @rollup/plugin-commonjs from 24.0.0 to 24.0.1 (#348) 80 | - maint(deps): bump vm2 from 3.9.15 to 3.9.16 (#360) 81 | - maint(deps): bump vm2 from 3.9.11 to 3.9.15 (#359) 82 | - maint(deps): bump cookiejar from 2.1.3 to 2.1.4 (#345) 83 | 84 | ## [4.0.1] - 2023-01-19 85 | 86 | ### Fixes 87 | 88 | - Use url-join instead of urljoin (#342) [@adamsmasher](https://github.com/adamsmasher) 89 | 90 | ### Maintenance 91 | 92 | - Add new project workflow (#321) | [@vreynolds](https://github.com/vreynolds) 93 | - Delete workflows for old board (#323) | [@vreynolds](https://github.com/vreynolds) 94 | - Add release file (#322) | [@vreynolds](https://github.com/vreynolds) 95 | - Update dependabot title with semantic commit format (#336) | [@pkanal](https://github.com/pkanal) 96 | - Update validate PR title workflow (#330) | [@pkanal](https://github.com/pkanal) 97 | - Validate PR title (#329) | [@pkanal](https://github.com/pkanal) 98 | 99 | ### Dependencies 100 | 101 | - Bump json5 from 2.2.1 to 2.2.3 (#341) 102 | - Bump qs and formidable (#335) 103 | - Bump babel-jest from 29.1.2 to 29.3.1 (#333) 104 | - Bump @rollup/plugin-json from 4.1.0 to 6.0.0 (#337) 105 | - Bump @rollup/plugin-replace from 4.0.0 to 5.0.2 (#338) 106 | - Bump @rollup/plugin-commonjs from 22.0.2 to 24.0.0 (#339) 107 | - Bump @babel/core from 7.19.3 to 7.20.12 (#343) 108 | - Bump @rollup/plugin-node-resolve from 14.1.0 to 15.0.1 (#325) 109 | - Bump eslint from 8.23.1 to 8.24.0 (#320) 110 | - Bump @babel/core from 7.19.0 to 7.19.3 (#319) 111 | - Bump superagent from 8.0.0 to 8.0.2 (#318) 112 | - Bump @babel/eslint-parser from 7.18.9 to 7.19.1 (#317) 113 | - Bump @babel/preset-env from 7.19.0 to 7.19.3 (#316) 114 | - Bump jest from 29.0.3 to 29.1.2 (#315) 115 | - Bump babel-jest from 29.0.3 to 29.1.2 (#313) 116 | - Bump vm2 from 3.9.7 to 3.9.11 (#312) 117 | 118 | 119 | ## [4.0.0] - 2022-09-19 120 | 121 | ### !!! Breaking Changes !!! 122 | 123 | - Drop Node v12, no longer security supported (#308) | [@emilyashley](https://github.com/emilyashley) 124 | 125 | ### Maintenance 126 | 127 | - Set circleCI Node default to latest v16 (#310) | [@emilyashley](https://github.com/emilyashley) 128 | 129 | ## [3.1.2] - 2022-09-13 130 | 131 | ### Maintenance 132 | 133 | - Add node version to the user-agent header (#299) | [@emilyashley](https://github.com/emilyashley) 134 | - Bump eslint from 8.17.0 to 8.23.1 (#300) 135 | - Bump @babel/core from 7.18.2 to 7.19.0 (#301) 136 | - Bump @rollup/plugin-node-resolve from 13.3.0 to 14.1.0 (#302) 137 | - Bump @babel/preset-env from 7.18.10 to 7.19.0 (#303) 138 | - Bump @babel/eslint-parser from 7.18.2 to 7.18.9 (#305) 139 | 140 | ## [3.1.1] - 2022-04-27 141 | 142 | ### Bug fixes 143 | 144 | - Update tests to properly terminate (#255) | [@kentquirk](https://github.com/kentquirk) 145 | - Handle `null` transmission in `flush` (#253) | [@sjchmiela](https://github.com/sjchmiela) 146 | 147 | ### Maintenance 148 | - maint: remove unused script (#252) | [@vreynolds](https://github.com/vreynolds) 149 | - Bump @rollup/plugin-commonjs from 21.0.1 to 21.0.3 (#248) 150 | - Bump @babel/preset-env from 7.16.8 to 7.16.11 (#229) 151 | - Bump superagent from 7.0.2 to 7.1.2 (#240) 152 | - Bump @babel/core from 7.16.12 to 7.17.9 (#245) 153 | - Bump @babel/eslint-parser from 7.16.5 to 7.17.0 (#251) 154 | - Bump prettier from 2.5.1 to 2.6.2 (#250) 155 | - Bump eslint from 8.6.0 to 8.13.0 (#249) 156 | - Bump @rollup/plugin-replace from 3.0.1 to 4.0.0 (#247) 157 | 158 | ## [3.1.0] - 2022-04-07 159 | 160 | ### Enhancements 161 | 162 | - Add support for environments (#244) | [@kentquirk](https://github.com/kentquirk) 163 | - ci: add node 17 to test matrix (#195) | [@vreynolds](https://github.com/vreynolds) 164 | - empower apply-labels action to apply labels (#205) | [@robbkidd](https://github.com/robbkidd) 165 | 166 | ### Maintenance 167 | 168 | - gh: add re-triage workflow (#215) | [@vreynolds](https://github.com/vreynolds) 169 | - Update dependabot.yml (#212) | [@vreynolds](https://github.com/vreynolds) 170 | - Bump @babel/core to 7.16.12 (#230) 171 | - Bump @babel/eslint-parser to 7.16.5 (#223) 172 | - Bump @babel/preset-env to 7.16.8 (#224) 173 | - Bump @rollup/plugin-commonjs to 21.0.1 (#197) 174 | - Bump @rollup/plugin-node-resolve to 13.1.3 (#225) 175 | - Bump @rollup/plugin-replace to 3.0.1 (#216) 176 | - Bump eslint to 8.6.0 (#227) 177 | - Bump minimist to 2.5.1 (#220) 178 | - Bump superagent from 6.1.0 to 7.0.2 (#226) 179 | - Bump vm2 to 3.9.7 (#234) 180 | 181 | ## [3.0.0] - 2021-10-18 182 | 183 | ### !!! Breaking Changes !!! 184 | 185 | - drop node 8 (#188) | [@vreynolds](https://github.com/vreynolds) 186 | 187 | ### Maintenance 188 | 189 | - Change maintenance badge to maintained (#186) | [@JamieDanielson](https://github.com/JamieDanielson) 190 | - Adds Stalebot (#187) | [@JamieDanielson](https://github.com/JamieDanielson) 191 | - Bump prettier from 2.4.0 to 2.4.1 (#184) 192 | - Bump tmpl from 1.0.4 to 1.0.5 (#185) 193 | 194 | ## [2.3.3] - 2021-09-16 195 | 196 | ### Maintenance 197 | 198 | - Bump prettier from 2.3.2 to 2.4.0 (#182) 199 | - Bump @babel/preset-env from 7.15.0 to 7.15.6 (#181) 200 | - Bump @babel/core from 7.15.0 to 7.15.5 (#179) 201 | - Bump husky from 7.0.1 to 7.0.2 (#176) 202 | - Bump superagent-proxy from 2.1.0 to 3.0.0 (#178) 203 | - Bump path-parse from 1.0.6 to 1.0.7 (#174) 204 | - Add note about dropping Node 8 in future (#177) 205 | - Add issue and PR templates (#175) 206 | - Add OSS lifecycle badge (#173) 207 | - Add community health files (#172) 208 | 209 | ## [2.3.2] - 2021-08-10 210 | 211 | ### Fixes 212 | 213 | - Remove yarn engine constraint introduced in v2.3.1 that prevented downstream 214 | projects from using yarn. (#170) | [@markandrus](https://github.com/markandrus) 215 | 216 | ## [2.3.1] - 2021-08-09 217 | 218 | ### Maintenance 219 | 220 | - Add node 16 to test matrix (#135) 221 | - Include all the test names when testing in CI (#125) 222 | - Switch from yarn to npm (#117) 223 | - Bump eslint from 6.5.1 to 7.25.0 (#122) 224 | - Bump lint-staged from 11.0.0 to 11.1.2 (#165) 225 | - Bump @babel/preset-env from 7.14.5 to 7.15.0 (#167) 226 | - Bump @babel/core from 7.14.6 to 7.15.0 (#166) 227 | - Bump eslint from 7.29.0 to 7.32.0 (#164) 228 | - Bump husky from 6.0.0 to 7.0.1 (#157) 229 | - Bump prettier from 2.3.1 to 2.3.2 (#150) 230 | - Bump @babel/core from 7.14.5 to 7.14.6 (#149) 231 | - Bump @babel/preset-env from 7.13.15 to 7.14.5 (#145) 232 | - Bump eslint from 7.28.0 to 7.29.0 (#148) 233 | - Bump @babel/core from 7.14.2 to 7.14.5 (#146) 234 | - Bump prettier from 1.19.1 to 2.3.1 (#144) 235 | - Bump ws from 5.2.2 to 5.2.3 (#147) 236 | - Bump eslint from 7.26.0 to 7.28.0 (#142) 237 | - Bump lint-staged from 7.3.0 to 11.0.0 (#132) 238 | - Bump browserslist from 4.16.4 to 4.16.6 (#136) 239 | - Bump eslint from 7.25.0 to 7.26.0 (#129) 240 | - Bump @babel/core from 7.13.15 to 7.14.2 (#130) 241 | - Bump superagent from 3.8.3 to 6.1.0 (#105) 242 | 243 | ## [2.3.0] - 2021-04-28 244 | 245 | ### Enhancements 246 | 247 | - add "stdout" transmission implementation (#119) | [@jharley](https://github.com/jharley) 248 | 249 | ### Fixed 250 | 251 | - fix npm publish (#110) | [@vreynolds](https://github.com/vreynolds) 252 | 253 | ### Maintenance 254 | 255 | - Bump y18n from 4.0.0 to 4.0.1 (#113) 256 | 257 | ## [2.2.2] - 2021-03-18 258 | 259 | ### Fixed 260 | 261 | - Improve transmission unit tests (#91) | [@DavidS](https://github.com/DavidS) 262 | - Use doc comment for WriterTransmission deprecation (#92) | [@DavidS](https://github.com/DavidS) 263 | - Change Builder.addDynamicField to match addField and docs (#89) | [@DavidS](https://github.com/DavidS) 264 | 265 | ### Maintenance 266 | 267 | - Bump @babel/core from 7.6.4 to 7.13.10 (#100) 268 | - Bump @babel/preset-env from 7.6.3 to 7.13.10 (#99) 269 | - Bump handlebars from 4.4.5 to 4.7.6 (#81) 270 | - Bump ini from 1.3.5 to 1.3.8 (#82) 271 | - Bump lodash from 4.17.15 to 4.17.20 (#83) 272 | - Bump yargs-parser from 13.1.1 to 13.1.2 (#84) 273 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the Honeycomb User Community Code of Conduct to clarify expected behavior in our community. 4 | 5 | https://www.honeycomb.io/honeycomb-user-community-code-of-conduct/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Please see our [general guide for OSS lifecycle and practices.](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Libhoney-js contributors: 2 | 3 | Ben Hartshorne 4 | Chris Toshok 5 | Christine Yen 6 | Josh Jarmain 7 | Ally Weir (@allyjweir) 8 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Local Development 2 | 3 | ## Requirements 4 | 5 | Node: https://nodejs.org/en/download 6 | 7 | ## Install Dependencies 8 | 9 | ```shell 10 | npm install 11 | ``` 12 | 13 | ## Run Tests 14 | 15 | To run all tests: 16 | 17 | ```shell 18 | npm run test 19 | ``` 20 | 21 | To run individual tests: 22 | 23 | ```shell 24 | npm run test -t transmission 25 | ``` 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | npm run build 3 | 4 | clean: 5 | rm -rf dist/* 6 | 7 | install: 8 | npm install 9 | 10 | lint: 11 | npm run lint 12 | 13 | format: 14 | npm run format 15 | 16 | test: 17 | npm run test 18 | 19 | smoke: 20 | @echo "" 21 | @echo "+++ Running example app in docker" 22 | @echo "" 23 | cd examples/factorial && docker-compose up --build --exit-code-from factorial-example 24 | 25 | unsmoke: 26 | @echo "" 27 | @echo "+++ Spinning down example app in docker" 28 | @echo "" 29 | cd examples/factorial && docker-compose down 30 | 31 | .PHONY: build clean install lint format test smoke unsmoke 32 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-Present Honeycomb, Hound Technology, Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=maintained 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libhoney 2 | 3 | [![OSS Lifecycle](https://img.shields.io/osslifecycle/honeycombio/libhoney-js?color=success)](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) 4 | [![CircleCI](https://circleci.com/gh/honeycombio/libhoney-js.svg?style=svg&circle-token=c7056d820eeaa624756e03c3da01deab9d647663)](https://circleci.com/gh/honeycombio/libhoney-js) 5 | [![npm version](https://badge.fury.io/js/libhoney.svg)](https://badge.fury.io/js/libhoney) 6 | 7 | A Node.js module for sending events to [Honeycomb](https://www.honeycomb.io), a service for debugging your software in production. 8 | For tracing support and automatic instrumentation of Express and other common libraries, check out our [Beeline for NodeJS](https://github.com/honeycombio/beeline-nodejs). 9 | 10 | [Usage and Examples](https://docs.honeycomb.io/getting-data-in/javascript/libhoney/) 11 | 12 | **NOTE** For use in browser-side JavaScript applications, generate an API key that has permission only to send events. 13 | 14 | ## Dependencies 15 | 16 | **Node 14.18+** 17 | 18 | ## Contributions 19 | 20 | See [DEVELOPMENT.md](./DEVELOPMENT.md) 21 | 22 | Features, bug fixes and other changes to libhoney are gladly accepted. Please 23 | open issues or a pull request with your change. 24 | 25 | All contributions will be released under the Apache License 2.0. 26 | 27 | ### Releasing a new version 28 | 29 | Use `npm version --no-git-tag-version` to update the version number using `major`, `minor`, `patch`, or the prerelease variants `premajor`, `preminor`, or `prepatch`. We use `--no-git-tag-version` to avoid automatically tagging - tagging with the version automatically triggers a CI run that publishes, and we only want to do that upon merging the PR into `main`. 30 | 31 | After doing this, follow our usual instructions for the actual process of tagging and releasing the package. 32 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | - Use `npm version --no-git-tag-version` to update the version number using `major`, `minor`, `patch`, or the prerelease variants `premajor`, `preminor`, or `prepatch`. 4 | For example, to bump from v1.1.1 to the next patch version: 5 | 6 | ```shell 7 | > npm version --no-git-tag-version patch # 1.1.1 -> 1.1.2 8 | ``` 9 | 10 | - Confirm the version number update appears in `package.json` and `package-lock.json`. 11 | - Update `CHANGELOG.md` with the changes since the last release. Consider automating with a command such as these two: 12 | - `git log $(git describe --tags --abbrev=0)..HEAD --no-merges --oneline > new-in-this-release.log` 13 | - `git log --pretty='%C(green)%d%Creset- %s | [%an](https://github.com/)'` 14 | - Commit changes, push, and open a release preparation pull request for review. 15 | - Once the pull request is merged, fetch the updated `main` branch. 16 | - Apply a tag for the new version on the merged commit (e.g. `git tag -a v2.3.1 -m "v2.3.1"`) 17 | - Push the tag upstream (this will kick off the release pipeline in CI) e.g. `git push origin v2.3.1` 18 | - Ensure that there is a draft GitHub release created as part of CI publish steps (this will also publish to NPM). 19 | - Click "generate release notes" in GitHub for full changelog notes and any new contributors 20 | - Publish the GitHub draft release 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This security policy applies to public projects under the [honeycombio organization][gh-organization] on GitHub. 4 | For security reports involving the services provided at `(ui|ui-eu|api|api-eu).honeycomb.io`, refer to the [Honeycomb Bug Bounty Program][bugbounty] for scope, expectations, and reporting procedures. 5 | 6 | ## Security/Bugfix Versions 7 | 8 | Security and bug fixes are generally provided only for the last minor version. 9 | Fixes are released either as part of the next minor version or as an on-demand patch version. 10 | 11 | Security fixes are given priority and might be enough to cause a new version to be released. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | We encourage responsible disclosure of security vulnerabilities. 16 | If you find something suspicious, we encourage and appreciate your report! 17 | 18 | ### Ways to report 19 | 20 | In order for the vulnerability reports to reach maintainers as soon as possible, the preferred way is to use the "Report a vulnerability" button under the "Security" tab of the associated GitHub project. 21 | This creates a private communication channel between the reporter and the maintainers. 22 | 23 | If you are absolutely unable to or have strong reasons not to use GitHub's vulnerability reporting workflow, please reach out to the Honeycomb security team at [security@honeycomb.io](mailto:security@honeycomb.io). 24 | 25 | [gh-organization]: https://github.com/honeycombio 26 | [bugbounty]: https://www.honeycomb.io/bugbountyprogram 27 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # How to Get Help 2 | 3 | This project uses GitHub issues to track bugs, feature requests, and questions about using the project. Please search for existing issues before filing a new one. 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { presets: ["@babel/preset-env"] }; 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | test: 5 | image: node:${NODE_VERSION:-16}-slim # Node 16 by default; "NODE_VERSION= docker-compose up" to run something different 6 | user: node 7 | working_dir: /home/node/app 8 | volumes: 9 | - ./:/home/node/app 10 | command: /bin/sh -c "while sleep 1000; do :; done" # idle, use "docker-compose exec test npm " after this is started 11 | ports: 12 | - 127.0.0.1:3000:3000 13 | 14 | squid: 15 | image: ubuntu/squid 16 | environment: 17 | - TZ=UTC 18 | ports: 19 | - 127.0.0.1:3128:3128 20 | 21 | socks: 22 | image: serjs/go-socks5-proxy 23 | ports: 24 | - 127.0.0.1:1080:1080 25 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Libhoney Examples 2 | 3 | You will need a Honeycomb API key to send data with the examples. You can find your API key [here](https://docs.honeycomb.io/working-with-your-data/settings/api-keys/#find-api-keys). 4 | 5 | To build and run these examples: 6 | 7 | 1. `npm install # install dependencies from the repo root` 8 | 1. `npm run build # build the libhoney package` 9 | 1. `cd $example-dir` 10 | 1. `npm install ../.. # this installs the libhoney packge you built above` 11 | 1. `npm install # install example dependencies` 12 | 1. `HONEYCOMB_API_KEY=YOUR_WRITE_KEY npm start` 13 | 14 | In a different terminal, you can then `curl --get http://localhost:3000/` to send a request to the running example, 15 | and then see the resulting telemetry at ui.honeycomb.io. 16 | -------------------------------------------------------------------------------- /examples/express-dynamic-fields/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /examples/express-dynamic-fields/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const express = require("express"); 3 | const honey = require("./express-honey"); 4 | const app = express(); 5 | 6 | app.use( 7 | honey({ 8 | writeKey: process.env["HONEYCOMB_API_KEY"], 9 | dataset: "express-example-dynamic-fields", 10 | sampleRate: 5 // log 1 out of every 5 events 11 | }) 12 | ); 13 | 14 | app.get("/", function(req, res) { 15 | res.send("Hello World!"); 16 | }); 17 | 18 | app.listen(3000, function() { 19 | console.log("Example app listening on port 3000!"); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/express-dynamic-fields/express-honey.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const libhoney = require("libhoney"); 3 | const process = require("process"); 4 | 5 | module.exports = function(options) { 6 | let honey = new libhoney(options); 7 | 8 | // Attach dynamic fields to the global event builder in libhoney. 9 | // Dynamic fields calculate their values at the time the event is created 10 | // (the event.send() call below) 11 | honey.addDynamicField("rss_after", () => process.memoryUsage().rss); 12 | honey.addDynamicField( 13 | "heapTotal_after", 14 | () => process.memoryUsage().heapTotal 15 | ); 16 | honey.addDynamicField("heapUsed_after", () => process.memoryUsage().heapUsed); 17 | 18 | return function(req, res, next) { 19 | let event = honey.newEvent(); 20 | event.add({ 21 | app: req.app, 22 | baseUrl: req.baseUrl, 23 | fresh: req.fresh, 24 | hostname: req.hostname, 25 | ip: req.ip, 26 | method: req.method, 27 | originalUrl: req.originalUrl, 28 | params: req.params, 29 | path: req.path, 30 | protocol: req.protocol, 31 | query: req.query, 32 | route: req.route, 33 | secure: req.secure, 34 | xhr: req.xhr, 35 | 36 | // these fields capture values for memory usage at the time they're added 37 | // to the newEvent 38 | rss_before: process.memoryUsage().rss, 39 | heapTotal_before: process.memoryUsage().heapTotal, 40 | heapUsed_before: process.memoryUsage().heapUsed 41 | }); 42 | 43 | next(); 44 | 45 | event.send(); 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /examples/express-dynamic-fields/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-dynamic-fields-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "express": "^4.13.4", 14 | "libhoney": "file:../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/express-response-time/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /examples/express-response-time/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const express = require("express"); 3 | const libhoney = require("libhoney"); 4 | const responseTime = require("response-time"); 5 | let app = express(); 6 | 7 | let honey = new libhoney({ 8 | writeKey: process.env["HONEYCOMB_API_KEY"], 9 | dataset: "express-example-response-time" 10 | }); 11 | 12 | app.use( 13 | responseTime(function(req, res, time) { 14 | honey.sendNow({ 15 | app: req.app, 16 | baseUrl: req.baseUrl, 17 | fresh: req.fresh, 18 | hostname: req.hostname, 19 | ip: req.ip, 20 | method: req.method, 21 | originalUrl: req.originalUrl, 22 | params: req.params, 23 | path: req.path, 24 | protocol: req.protocol, 25 | query: req.query, 26 | route: req.route, 27 | secure: req.secure, 28 | xhr: req.xhr, 29 | responseTime_ms: time 30 | }); 31 | }) 32 | ); 33 | 34 | app.get("/", function(req, res) { 35 | res.send("Hello World!"); 36 | }); 37 | 38 | app.listen(3000, function() { 39 | console.log("Example app listening on port 3000!"); 40 | }); 41 | -------------------------------------------------------------------------------- /examples/express-response-time/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-response-time-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "express": "^4.13.4", 14 | "libhoney": "file:../..", 15 | "response-time": "^2.3.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/express/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /examples/express/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const express = require("express"); 3 | const honey = require("./express-honey"); 4 | let app = express(); 5 | 6 | app.use( 7 | honey({ 8 | writeKey: process.env["HONEYCOMB_API_KEY"], 9 | // proxy: "http://localhost:3128", 10 | dataset: "express-example" 11 | }) 12 | ); 13 | 14 | app.get("/", function(req, res) { 15 | res.send("Hello World!"); 16 | }); 17 | 18 | app.listen(3000, function() { 19 | console.log("Example app listening on port 3000!"); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/express/express-honey.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const libhoney = require("libhoney"); 3 | 4 | module.exports = function(options) { 5 | let honey = new libhoney(options); 6 | 7 | return function(req, res, next) { 8 | const responseCallback = queue => { 9 | let responses = queue.splice(0, queue.length); 10 | for (let i = 0; i < responses.length; i++) { 11 | console.log("response status =", responses[i].status_code); 12 | } 13 | }; 14 | honey.once("response", responseCallback); 15 | 16 | honey.sendNow({ 17 | app: req.app, 18 | baseUrl: req.baseUrl, 19 | fresh: req.fresh, 20 | hostname: req.hostname, 21 | ip: req.ip, 22 | method: req.method, 23 | originalUrl: req.originalUrl, 24 | params: req.params, 25 | path: req.path, 26 | protocol: req.protocol, 27 | query: req.query, 28 | route: req.route, 29 | secure: req.secure, 30 | xhr: req.xhr, 31 | dataset: honey._options.dataset || "express-example", 32 | proxy: honey._options.proxy || "none", 33 | }); 34 | next(); 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-express-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "express": "^4.13.4", 14 | "libhoney": "file:../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/factorial/FactorialDockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # Create app directory 4 | WORKDIR /app 5 | 6 | COPY . . 7 | 8 | # Install libhoney 9 | RUN npm install 10 | RUN npm run build 11 | 12 | # Install example dependencies 13 | RUN cd ./examples/factorial && npm install 14 | 15 | EXPOSE 3000 16 | CMD [ "npm", "--prefix", "./examples/factorial", "start" ] 17 | -------------------------------------------------------------------------------- /examples/factorial/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | 3 | x-env-base: &env_base 4 | HONEYCOMB_API_ENDPOINT: test_endpoint 5 | HONEYCOMB_API_KEY: test_key 6 | HONEYCOMB_DATASET: test_dataset 7 | 8 | x-app-base: &app_base 9 | build: 10 | context: ../../ 11 | dockerfile: ./examples/factorial/FactorialDockerfile 12 | image: hnyexample/factorial-example 13 | 14 | services: 15 | factorial-example: 16 | <<: *app_base 17 | environment: 18 | <<: *env_base 19 | -------------------------------------------------------------------------------- /examples/factorial/factorial.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const libhoney = require("libhoney"); 3 | 4 | let honey = new libhoney({ 5 | writeKey: process.env["HONEYCOMB_API_KEY"], 6 | dataset: "express-example" 7 | }); 8 | 9 | function factorial(n) { 10 | honey.sendNow({ 11 | factorialNum: n 12 | }); 13 | if (n < 0) { 14 | return -1 * factorial(Math.abs(n)); 15 | } 16 | if (n === 0) { 17 | return 1; 18 | } 19 | return n * factorial(n - 1); 20 | } 21 | 22 | console.log("Starting factorial example..."); 23 | 24 | factorial(10); 25 | 26 | console.log("Finished factorial example!"); 27 | -------------------------------------------------------------------------------- /examples/factorial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "factorial-libhoney-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "factorial.js", 6 | "scripts": { 7 | "start": "node factorial.js" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "libhoney": "file:../.." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libhoney", 3 | "version": "4.3.1", 4 | "description": " Honeycomb.io Javascript library", 5 | "bugs": "https://github.com/honeycombio/libhoney-js/issues", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/honeycombio/libhoney-js.git" 9 | }, 10 | "engines": { 11 | "node": ">= 14.18" 12 | }, 13 | "browser": "dist/libhoney.browser.js", 14 | "module": "dist/libhoney.es.js", 15 | "main": "dist/libhoney.cjs.js", 16 | "files": [ 17 | "dist", 18 | "README.md", 19 | "LICENSE" 20 | ], 21 | "scripts": { 22 | "build": "npm run build:node && npm run build:browser", 23 | "build:node": "rollup -c rollup.config.js", 24 | "build:browser": "rollup -c rollup.browser.config.js", 25 | "dev": "rollup -c -w", 26 | "test": "jest", 27 | "test-coverage": "jest --coverage", 28 | "format": "prettier --write \"src/**/*.js\" rollup.config.js rollup.browser.config.js", 29 | "check-format": "prettier \"src/**/*.js\" rollup.config.js rollup.browser.config.js", 30 | "lint": "eslint \"src/**/*.js\" rollup.config.js rollup.browser.config.js" 31 | }, 32 | "author": "", 33 | "license": "Apache-2.0", 34 | "devDependencies": { 35 | "@babel/core": "^7.4.0", 36 | "@babel/eslint-parser": "^7.15.8", 37 | "@babel/preset-env": "^7.4.2", 38 | "@rollup/plugin-commonjs": "^28.0.1", 39 | "@rollup/plugin-json": "^6.0.0", 40 | "@rollup/plugin-node-resolve": "^15.0.1", 41 | "@rollup/plugin-replace": "^5.0.2", 42 | "babel-jest": "^29.0.3", 43 | "babel-polyfill": "^6.26.0", 44 | "eslint": "^8.0.1", 45 | "jest": "^29.0.3", 46 | "jest-in-case": "^1.0.2", 47 | "prettier": "^3.0.0", 48 | "regenerator-runtime": "^0.13.9", 49 | "rollup": "^3.20.2", 50 | "superagent-mocker": "^0.5.2" 51 | }, 52 | "dependencies": { 53 | "proxy-agent": "^6.3.0", 54 | "superagent": "^10.1.1", 55 | "url-join": "^5.0.0" 56 | }, 57 | "jest": { 58 | "setupFilesAfterEnv": [ 59 | "./setupTests.js" 60 | ], 61 | "transformIgnorePatterns": [ 62 | "/node_modules/(?!url-join)" 63 | ], 64 | "testPathIgnorePatterns": [ 65 | "dist/", 66 | "/node_modules/" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rollup.browser.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const { nodeResolve } = require("@rollup/plugin-node-resolve"); 3 | const commonjs = require("@rollup/plugin-commonjs"); 4 | const replace = require("@rollup/plugin-replace"); 5 | const json = require("@rollup/plugin-json"); 6 | const pkg = require("./package.json"); 7 | 8 | module.exports = { 9 | input: "src/libhoney.js", 10 | external: ["superagent", "events", "path", "url"], 11 | 12 | plugins: [ 13 | nodeResolve(), 14 | commonjs(), 15 | json(), 16 | replace({ 17 | delimiters: ["<@", "@>"], 18 | LIBHONEY_JS_VERSION: pkg.version, 19 | }), 20 | replace({ 21 | "process.env.LIBHONEY_TARGET": '"browser"', 22 | }), 23 | ], 24 | 25 | output: [{ file: pkg.browser, format: "cjs" }], 26 | }; 27 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const { nodeResolve } = require("@rollup/plugin-node-resolve"); 3 | const commonjs = require("@rollup/plugin-commonjs"); 4 | const replace = require("@rollup/plugin-replace"); 5 | const json = require("@rollup/plugin-json"); 6 | const pkg = require("./package.json"); 7 | 8 | module.exports = { 9 | input: "src/libhoney.js", 10 | external: ["superagent", "events", "path", "url", "proxy-agent"], 11 | 12 | plugins: [ 13 | nodeResolve(), 14 | commonjs(), 15 | json(), 16 | replace({ 17 | delimiters: ["<@", "@>"], 18 | LIBHONEY_JS_VERSION: pkg.version, 19 | }), 20 | replace({ 21 | "process.env.LIBHONEY_TARGET": '"node"', 22 | }), 23 | ], 24 | 25 | output: [ 26 | { file: pkg.main, format: "cjs" }, 27 | { file: pkg.module, format: "es" }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | -------------------------------------------------------------------------------- /src/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/__tests__/builder_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | import libhoney from "../libhoney"; 3 | 4 | describe("libhoney builder", () => { 5 | let hny = new libhoney(); 6 | 7 | it("takes fields and dynamic fields in ctor", () => { 8 | let b = hny.newBuilder( 9 | { a: 5 }, 10 | { 11 | b: function() { 12 | return 3; 13 | } 14 | } 15 | ); 16 | expect(b._fields.a).toEqual(5); 17 | expect(b._fields.b).toEqual(undefined); 18 | b = hny.newBuilder(); 19 | expect(Object.getOwnPropertyNames(b._fields)).toHaveLength(0); 20 | expect(Object.getOwnPropertyNames(b._dynFields)).toHaveLength(0); 21 | }); 22 | 23 | it("accepts dict-like arguments to .add()", () => { 24 | let b; 25 | let ev; 26 | 27 | b = hny.newBuilder(); 28 | b.add({ a: 5 }); 29 | ev = b.newEvent(); 30 | expect(ev.data.a).toEqual(5); 31 | 32 | let map = new Map(); 33 | map.set("a", 5); 34 | b = hny.newBuilder(); 35 | b.add(map); 36 | ev = b.newEvent(); 37 | expect(ev.data.a).toEqual(5); 38 | }); 39 | 40 | it("doesn't stringify object values", () => { 41 | let honey = new libhoney({ 42 | apiHost: "http://foo/bar", 43 | writeKey: "12345", 44 | dataset: "testing", 45 | transmission: "mock" 46 | }); 47 | let transmission = honey.transmission; 48 | 49 | let postData = { a: { b: 1 }, c: { d: 2 } }; 50 | 51 | let builder = honey.newBuilder({ a: { b: 1 } }); 52 | 53 | builder.sendNow({ c: { d: 2 } }); 54 | 55 | expect(transmission.events).toHaveLength(1); 56 | expect(transmission.events[0].postData).toEqual(postData); 57 | }); 58 | 59 | it("includes snapshot of global fields/dynFields", () => { 60 | let honey = new libhoney({ 61 | apiHost: "http://foo/bar", 62 | writeKey: "12345", 63 | dataset: "testing", 64 | transmission: "mock" 65 | }); 66 | let transmission = honey.transmission; 67 | 68 | let postData = { b: 2, c: 3 }; 69 | 70 | let builder = honey.newBuilder({ b: 2 }); 71 | 72 | // add a global field *after* creating the builder. it shouldn't show up in the post data 73 | honey.addField("a", 1); 74 | 75 | builder.sendNow({ c: 3 }); 76 | 77 | expect(transmission.events).toHaveLength(1); 78 | expect(transmission.events[0].postData).toEqual(postData); 79 | 80 | // but if we create another builder, it should show up in the post data. 81 | postData = { a: 1, b: 2, c: 3 }; 82 | 83 | builder = honey.newBuilder({ b: 2 }); 84 | 85 | builder.sendNow({ c: 3 }); 86 | 87 | expect(transmission.events).toHaveLength(2); 88 | expect(transmission.events[1].postData).toEqual(postData); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/__tests__/event_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | import libhoney from "../libhoney"; 3 | 4 | describe("libhoney events", () => { 5 | let hny = new libhoney(); 6 | 7 | it("inherit fields and dyn_fields from builder", () => { 8 | let b = hny.newBuilder( 9 | { a: 5 }, 10 | { 11 | b: function() { 12 | return 3; 13 | } 14 | } 15 | ); 16 | 17 | let ev = b.newEvent(); 18 | expect(ev.data.a).toEqual(5); 19 | expect(ev.data.b).toEqual(3); 20 | }); 21 | 22 | it("accepts dict-like arguments to .add()", () => { 23 | let b = hny.newBuilder(); 24 | let ev = b.newEvent(); 25 | 26 | ev.add({ a: 5 }); 27 | expect(ev.data.a).toEqual(5); 28 | 29 | let ev2 = b.newEvent(); 30 | let map = new Map(); 31 | map.set("a", 5); 32 | ev2.add(map); 33 | expect(ev2.data.a).toEqual(5); 34 | }); 35 | 36 | it("it toString()'s keys from Maps in .add()", () => { 37 | let b = hny.newBuilder(); 38 | let ev = b.newEvent(); 39 | 40 | let map = new Map(); 41 | map.set( 42 | { 43 | toString: function() { 44 | return "hello"; 45 | } 46 | }, 47 | 5 48 | ); 49 | ev.add(map); 50 | 51 | expect(ev.data.hello).toEqual(5); 52 | }); 53 | 54 | it("doesn't stringify object values", () => { 55 | let postData = { c: { a: 1 } }; 56 | 57 | let ev = hny.newEvent(); 58 | 59 | ev.add(postData); 60 | 61 | expect(JSON.stringify(ev.data)).toEqual(JSON.stringify(postData)); 62 | }); 63 | 64 | it("converts all values to primitive types in .add/.addField", () => { 65 | let b = hny.newBuilder(); 66 | let ev; 67 | let map; 68 | 69 | ev = b.newEvent(); 70 | map = new Map(); 71 | 72 | // Object, we pass it on through (and let Honeycomb serialize it if 73 | // necessary) 74 | map.set("obj", { a: 1, b: 2 }); 75 | 76 | // String converts to a string 77 | map.set("String", new String("a:1")); 78 | map.set("string", "a:1"); 79 | 80 | // Number converts to a number 81 | map.set("Number", new Number(5)); 82 | map.set("number", 5); 83 | 84 | // Boolean converts to a boolean 85 | map.set("Boolean", new Boolean(true)); 86 | map.set("boolean", true); 87 | 88 | // Date does not convert 89 | let d = new Date(1, 2, 3, 4, 5, 6, 7); 90 | map.set("Date", d); 91 | 92 | // Null/undefined both end up being null in the output 93 | map.set("null", null); 94 | map.set("undefined", undefined); 95 | 96 | ev.add(map); 97 | 98 | expect(JSON.stringify(ev.data)).toEqual( 99 | `{"obj":{"a":1,"b":2},"String":"a:1","string":"a:1","Number":5,"number":5,"Boolean":true,"boolean":true,"Date":${JSON.stringify( 100 | d 101 | )},"null":null,"undefined":null}` 102 | ); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/__tests__/libhoney_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | import { MockTransmission } from "../transmission"; 3 | import libhoney from "../libhoney"; 4 | 5 | let superagent = require("superagent"); 6 | let mock = require("superagent-mocker")(superagent); 7 | 8 | describe("libhoney", () => { 9 | describe("constructor options", () => { 10 | describe.each(["mock", MockTransmission])( 11 | "with %p transmission", 12 | (transmissionSpec) => { 13 | it("should be communicated to transmission constructor", () => { 14 | const options = { 15 | a: 1, 16 | b: 2, 17 | c: 3, 18 | d: 4, 19 | transmission: transmissionSpec, 20 | }; 21 | 22 | const honey = new libhoney(options); 23 | 24 | const transmission = honey.transmission; 25 | 26 | expect(options.a).toEqual(transmission.constructorArg.a); 27 | expect(options.b).toEqual(transmission.constructorArg.b); 28 | expect(options.c).toEqual(transmission.constructorArg.c); 29 | expect(options.d).toEqual(transmission.constructorArg.d); 30 | }); 31 | } 32 | ); 33 | }); 34 | 35 | describe("event properties", () => { 36 | it("should ultimately fallback to hardcoded defaults", () => { 37 | let honey = new libhoney({ 38 | // these two properties are required 39 | writeKey: "12345", 40 | dataset: "testing", 41 | transmission: "mock", 42 | }); 43 | let transmission = honey.transmission; 44 | let postData = { a: 1, b: 2 }; 45 | honey.sendNow(postData); 46 | 47 | expect(transmission.events).toHaveLength(1); 48 | expect(transmission.events[0].apiHost).toEqual( 49 | "https://api.honeycomb.io/" 50 | ); 51 | expect(transmission.events[0].writeKey).toEqual("12345"); 52 | 53 | expect(transmission.events[0].dataset).toEqual("testing"); 54 | expect(transmission.events[0].sampleRate).toEqual(1); 55 | expect(transmission.events[0].timestamp).toBeInstanceOf(Date); 56 | expect(transmission.events[0].postData).toEqual(postData); 57 | }); 58 | 59 | it("should come from libhoney options if not specified in event", () => { 60 | let honey = new libhoney({ 61 | apiHost: "http://foo/bar", 62 | writeKey: "12345", 63 | dataset: "testing", 64 | transmission: "mock", 65 | }); 66 | let transmission = honey.transmission; 67 | let postData = { a: 1, b: 2 }; 68 | honey.sendNow(postData); 69 | 70 | expect(transmission.events).toHaveLength(1); 71 | expect(transmission.events[0].apiHost).toEqual("http://foo/bar"); 72 | expect(transmission.events[0].writeKey).toEqual("12345"); 73 | expect(transmission.events[0].dataset).toEqual("testing"); 74 | expect(transmission.events[0].postData).toEqual(postData); 75 | }); 76 | 77 | it("should reject a send from an empty dataset with a classic key", () => { 78 | // mock out console.error 79 | console.error = jest.fn(); 80 | 81 | let honey = new libhoney({ 82 | apiHost: "http://foo/bar", 83 | writeKey: "12345678901234567890123456789012", 84 | dataset: "", 85 | transmission: "mock", 86 | }); 87 | let transmission = honey.transmission; 88 | let postData = { a: 1, b: 2 }; 89 | honey.sendNow(postData); 90 | 91 | expect(transmission.events).toHaveLength(0); 92 | expect(console.error.mock.calls[0][0]).toBe( 93 | "dataset must be a non-empty string" 94 | ); 95 | }); 96 | 97 | it("should reject a send from an empty dataset with a classic v3 key", () => { 98 | // mock out console.error 99 | console.error = jest.fn(); 100 | 101 | const classicv3IngestKey = "hcaic_1234567890123456789012345678901234567890123456789012345678"; 102 | 103 | let honey = new libhoney({ 104 | apiHost: "http://foo/bar", 105 | writeKey: classicv3IngestKey, 106 | dataset: "", 107 | transmission: "mock", 108 | }); 109 | let transmission = honey.transmission; 110 | let postData = { a: 1, b: 2 }; 111 | honey.sendNow(postData); 112 | 113 | expect(transmission.events).toHaveLength(0); 114 | expect(console.error.mock.calls[0][0]).toBe( 115 | "dataset must be a non-empty string" 116 | ); 117 | }); 118 | 119 | it("should set an empty dataset to unknown_dataset with a V2 key", () => { 120 | let honey = new libhoney({ 121 | apiHost: "http://foo/bar", 122 | writeKey: "aKeySimilarToOurV2Keys", 123 | dataset: "", 124 | transmission: "mock", 125 | }); 126 | let transmission = honey.transmission; 127 | let postData = { a: 1, b: 2 }; 128 | honey.sendNow(postData); 129 | 130 | expect(transmission.events).toHaveLength(1); 131 | expect(transmission.events[0].apiHost).toEqual("http://foo/bar"); 132 | expect(transmission.events[0].writeKey).toEqual("aKeySimilarToOurV2Keys"); 133 | expect(transmission.events[0].dataset).toEqual("unknown_dataset"); 134 | expect(transmission.events[0].postData).toEqual(postData); 135 | }); 136 | }); 137 | 138 | describe("response queue", () => { 139 | it("should enqueue a maximum of maxResponseQueueSize, dropping new responses (not old)", (done) => { 140 | mock.post("http://localhost:9999/1/events/testResponseQueue", (_req) => { 141 | return {}; 142 | }); 143 | 144 | let queueSize = 50; 145 | let queueFullCount = 0; 146 | let honey = new libhoney({ 147 | apiHost: "http://localhost:9999", 148 | writeKey: "12345", 149 | dataset: "testResponseQueue", 150 | maxResponseQueueSize: queueSize, 151 | }); 152 | 153 | // we send queueSize+1 events, so we should see two response events 154 | // with queueSize as the length 155 | honey.on("response", (queue) => { 156 | if (queue.length !== queueSize) { 157 | return; 158 | } 159 | 160 | queueFullCount++; 161 | if (queueFullCount === 2) { 162 | queue.sort((a, b) => a.metadata - b.metadata); 163 | expect(queue[0].metadata).toEqual(0); 164 | expect(queue[queueSize - 1].metadata).toEqual(queueSize - 1); 165 | done(); 166 | } 167 | }); 168 | 169 | for (let i = 0; i < queueSize + 1; i++) { 170 | let ev = honey.newEvent(); 171 | ev.add({ a: 1, b: 2 }); 172 | ev.addMetadata(i); 173 | ev.send(); 174 | } 175 | }); 176 | }); 177 | 178 | describe("disabled = true", () => { 179 | it("should not hit transmission", async () => { 180 | let honey = new libhoney({ 181 | // these two properties are required 182 | writeKey: "12345", 183 | dataset: "testing", 184 | transmission: "mock", 185 | disabled: true, 186 | }); 187 | let transmission = honey.transmission; 188 | 189 | expect(transmission).toBe(null); 190 | await expect(honey.flush()).resolves.toBeUndefined(); 191 | }); 192 | }); 193 | }); 194 | 195 | describe("isClassic check", () => { 196 | it.each([ 197 | { 198 | testString: "hcxik_01hqk4k20cjeh63wca8vva5stw70nft6m5n8wr8f5mjx3762s8269j50wc", 199 | name: "full ingest key string, non classic", 200 | expected: false 201 | }, 202 | { 203 | testString: "hcxik_01hqk4k20cjeh63wca8vva5stw", 204 | name: "ingest key id, non classic", 205 | expected: false 206 | }, 207 | { 208 | testString: "hcaic_1234567890123456789012345678901234567890123456789012345678", 209 | name: "full ingest key string, classic", 210 | expected: true 211 | }, 212 | { 213 | testString: "hcaic_12345678901234567890123456", 214 | name: "ingest key id, classic", 215 | expected: false 216 | }, 217 | { 218 | testString: "kgvSpPwegJshQkuowXReLD", 219 | name: "v2 configuration key", 220 | expected: false 221 | }, 222 | { 223 | testString: "12345678901234567890123456789012", 224 | name: "classic key", 225 | expected: true 226 | }, 227 | { 228 | testString: "", 229 | name: "no key", 230 | expected: true 231 | } 232 | 233 | ])("test case $name", (testCase) => { 234 | expect(libhoney.isClassic(testCase.testString)).toEqual(testCase.expected); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /src/__tests__/transmission_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | import "babel-polyfill"; 3 | 4 | import { Transmission, ValidatedEvent } from "../transmission"; 5 | 6 | import http from "http"; 7 | import net from "net"; 8 | import superagent from "superagent"; 9 | import superagentMocker from "superagent-mocker"; 10 | 11 | let mock; 12 | 13 | describe("base transmission", () => { 14 | beforeEach(() => (mock = superagentMocker(superagent))); 15 | afterEach(() => { 16 | mock.clearRoutes(); 17 | mock.unmock(superagent); 18 | }); 19 | 20 | // This checks that the code connects to a proxy 21 | it("will hit a proxy", done => { 22 | let server = net.createServer(socket => { 23 | // if we get here, we got data, so the test passes -- otherwise, 24 | // the test will never end and will timeout, which is a failure. 25 | socket.destroy(); 26 | server.close(() => { 27 | done(); 28 | }); 29 | }); 30 | 31 | server.listen(9998, "127.0.0.1"); 32 | 33 | let transmission = new Transmission({ 34 | proxy: "http://127.0.0.1:9998", 35 | batchTimeTrigger: 10000, // larger than the mocha timeout 36 | batchSizeTrigger: 0 37 | }); 38 | 39 | transmission.sendEvent( 40 | new ValidatedEvent({ 41 | apiHost: "http://localhost:9999", 42 | writeKey: "123456789", 43 | dataset: "test-transmission", 44 | sampleRate: 1, 45 | timestamp: new Date(), 46 | postData: { a: 1, b: 2 } 47 | }) 48 | ); 49 | }); 50 | 51 | it("will share its http info with a proxy", done => { 52 | const proxyServer = http.createServer((req, res) => { 53 | res.writeHead(418, { "Content-Type": "application/json" }); 54 | res.end("[{ status: 418 }]"); 55 | expect(req.headers.host).toBe("localhost:1234"); 56 | expect(req.method).toBe("POST"); 57 | expect(req.headers["x-honeycomb-team"]).toBe("123456789"); 58 | proxyServer.close(() => { 59 | proxyServer.removeAllListeners(); 60 | done(); 61 | }); 62 | }); 63 | 64 | proxyServer.listen(9998, "127.0.0.1"); 65 | 66 | let transmission = new Transmission({ 67 | proxy: "http://127.0.0.1:9998", 68 | batchTimeTrigger: 10000, // larger than the mocha timeout 69 | batchSizeTrigger: 0, 70 | }); 71 | 72 | 73 | transmission.sendEvent( 74 | new ValidatedEvent({ 75 | apiHost: "http://localhost:1234", 76 | writeKey: "123456789", 77 | dataset: "test-transmission", 78 | sampleRate: 1, 79 | timestamp: new Date(), 80 | postData: { a: 1, b: 2 } 81 | }) 82 | ); 83 | }); 84 | 85 | it("should handle batchSizeTrigger of 0", done => { 86 | mock.post("http://localhost:9999/1/events/test-transmission", req => { 87 | let reqEvents = JSON.parse(req.body); 88 | let resp = reqEvents.map(() => ({ status: 202 })); 89 | return { text: JSON.stringify(resp) }; 90 | }); 91 | 92 | let transmission = new Transmission({ 93 | batchTimeTrigger: 10000, // larger than the mocha timeout 94 | batchSizeTrigger: 0, 95 | responseCallback() { 96 | done(); 97 | } 98 | }); 99 | 100 | transmission.sendEvent( 101 | new ValidatedEvent({ 102 | apiHost: "http://localhost:9999", 103 | writeKey: "123456789", 104 | dataset: "test-transmission", 105 | sampleRate: 1, 106 | timestamp: new Date(), 107 | postData: { a: 1, b: 2 } 108 | }) 109 | ); 110 | }); 111 | 112 | it("should send a batch when batchSizeTrigger is met, not exceeded", done => { 113 | let responseCount = 0; 114 | let batchSize = 5; 115 | 116 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 117 | let reqEvents = JSON.parse(req.body); 118 | let resp = reqEvents.map(() => ({ status: 202 })); 119 | return { text: JSON.stringify(resp) }; 120 | }); 121 | 122 | let transmission = new Transmission({ 123 | batchTimeTrigger: 10000, // larger than the mocha timeout 124 | batchSizeTrigger: 5, 125 | responseCallback(queue) { 126 | responseCount += queue.length; 127 | queue.splice(0, queue.length); 128 | return responseCount === batchSize 129 | ? done() 130 | : done( 131 | "The events dispatched over transmission does not align with batch size when the same number of " + 132 | `events were enqueued as the batchSizeTrigger. Expected ${batchSize}, got ${responseCount}.` 133 | ); 134 | } 135 | }); 136 | 137 | for (let i = 0; i < batchSize; i++) { 138 | transmission.sendEvent( 139 | new ValidatedEvent({ 140 | apiHost: "http://localhost:9999", 141 | writeKey: "123456789", 142 | dataset: "test-transmission", 143 | sampleRate: 1, 144 | timestamp: new Date(), 145 | postData: { a: 1, b: 2 } 146 | }) 147 | ); 148 | } 149 | }); 150 | 151 | it("should handle apiHosts with trailing slashes", done => { 152 | let endpointHit = false; 153 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 154 | endpointHit = true; 155 | let reqEvents = JSON.parse(req.body); 156 | let resp = reqEvents.map(() => ({ status: 202 })); 157 | return { text: JSON.stringify(resp) }; 158 | }); 159 | 160 | let transmission = new Transmission({ 161 | batchTimeTrigger: 0, 162 | responseCallback: function (_resp) { 163 | expect(endpointHit).toBe(true); 164 | done(); 165 | } 166 | }); 167 | 168 | transmission.sendEvent( 169 | new ValidatedEvent({ 170 | apiHost: "http://localhost:9999/", 171 | writeKey: "123456789", 172 | dataset: "test-transmission", 173 | sampleRate: 1, 174 | timestamp: new Date(), 175 | postData: { a: 1, b: 2 } 176 | }) 177 | ); 178 | }); 179 | 180 | it("should eventually send a single event (after the timeout)", done => { 181 | let transmission = new Transmission({ 182 | batchTimeTrigger: 10, 183 | responseCallback: function (_resp) { 184 | done(); 185 | } 186 | }); 187 | 188 | transmission.sendEvent( 189 | new ValidatedEvent({ 190 | apiHost: "http://localhost:9999", 191 | writeKey: "123456789", 192 | dataset: "test-transmission", 193 | sampleRate: 1, 194 | timestamp: new Date(), 195 | postData: { a: 1, b: 2 } 196 | }) 197 | ); 198 | }); 199 | 200 | it("should respect sample rate and accept the event", done => { 201 | let transmission = new Transmission({ 202 | batchTimeTrigger: 10, 203 | responseCallback: function (_resp) { 204 | done(); 205 | } 206 | }); 207 | 208 | transmission._randomFn = function () { 209 | return 0.09; 210 | }; 211 | transmission.sendEvent( 212 | new ValidatedEvent({ 213 | apiHost: "http://localhost:9999", 214 | writeKey: "123456789", 215 | dataset: "test-transmission", 216 | sampleRate: 10, 217 | timestamp: new Date(), 218 | postData: { a: 1, b: 2 } 219 | }) 220 | ); 221 | }); 222 | 223 | it("should respect sample rate and drop the event", done => { 224 | let transmission = new Transmission({ batchTimeTrigger: 10 }); 225 | 226 | transmission._randomFn = function () { 227 | return 0.11; 228 | }; 229 | transmission._droppedCallback = function () { 230 | done(); 231 | }; 232 | 233 | transmission.sendEvent( 234 | new ValidatedEvent({ 235 | apiHost: "http://localhost:9999", 236 | writeKey: "123456789", 237 | dataset: "test-transmission", 238 | sampleRate: 10, 239 | timestamp: new Date(), 240 | postData: { a: 1, b: 2 } 241 | }) 242 | ); 243 | }); 244 | 245 | it("should drop events beyond the pendingWorkCapacity", done => { 246 | let eventDropped; 247 | let droppedExpected = 5; 248 | let responseCount = 0; 249 | let responseExpected = 5; 250 | 251 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 252 | let reqEvents = JSON.parse(req.body); 253 | let resp = reqEvents.map(() => ({ status: 202 })); 254 | return { text: JSON.stringify(resp) }; 255 | }); 256 | 257 | let transmission = new Transmission({ 258 | batchTimeTrigger: 50, 259 | pendingWorkCapacity: responseExpected, 260 | responseCallback(queue) { 261 | responseCount += queue.length; 262 | queue.splice(0, queue.length); 263 | if (responseCount === responseExpected) { 264 | done(); 265 | } 266 | } 267 | }); 268 | 269 | transmission._droppedCallback = function () { 270 | eventDropped = true; 271 | }; 272 | 273 | // send the events we expect responses for 274 | for (let i = 0; i < responseExpected; i++) { 275 | transmission.sendEvent( 276 | new ValidatedEvent({ 277 | apiHost: "http://localhost:9999", 278 | writeKey: "123456789", 279 | dataset: "test-transmission", 280 | sampleRate: 1, 281 | timestamp: new Date(), 282 | postData: { a: 1, b: 2 } 283 | }) 284 | ); 285 | } 286 | 287 | // send the events we expect to drop. Since JS is single threaded we can 288 | // verify that droppedCount behaves the way we want. 289 | for (let i = 0; i < droppedExpected; i++) { 290 | eventDropped = false; 291 | transmission.sendEvent( 292 | new ValidatedEvent({ 293 | apiHost: "http://localhost:9999", 294 | writeKey: "123456789", 295 | dataset: "test-transmission", 296 | sampleRate: 1, 297 | timestamp: new Date(), 298 | postData: { a: 1, b: 2 } 299 | }) 300 | ); 301 | expect(eventDropped).toBe(true); 302 | } 303 | }); 304 | 305 | it("should send the right number events even if it requires multiple concurrent batches", done => { 306 | let responseCount = 0; 307 | let responseExpected = 10; 308 | 309 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 310 | let reqEvents = JSON.parse(req.body); 311 | let resp = reqEvents.map(() => ({ status: 202 })); 312 | return { text: JSON.stringify(resp) }; 313 | }); 314 | 315 | let transmission = new Transmission({ 316 | batchTimeTrigger: 50, 317 | batchSizeTrigger: 5, 318 | pendingWorkCapacity: responseExpected, 319 | responseCallback(queue) { 320 | responseCount += queue.length; 321 | queue.splice(0, queue.length); 322 | if (responseCount === responseExpected) { 323 | done(); 324 | } 325 | } 326 | }); 327 | 328 | for (let i = 0; i < responseExpected; i++) { 329 | transmission.sendEvent( 330 | new ValidatedEvent({ 331 | apiHost: "http://localhost:9999", 332 | writeKey: "123456789", 333 | dataset: "test-transmission", 334 | sampleRate: 1, 335 | timestamp: new Date(), 336 | postData: { a: 1, b: 2 } 337 | }) 338 | ); 339 | } 340 | }); 341 | 342 | it("should send the right number of events even if they all fail", done => { 343 | let responseCount = 0; 344 | let responseExpected = 10; 345 | 346 | mock.post("http://localhost:9999/1/batch/test-transmission", _req => { 347 | return { status: 404 }; 348 | }); 349 | 350 | let transmission = new Transmission({ 351 | batchTimeTrigger: 50, 352 | batchSizeTrigger: 5, 353 | maxConcurrentBatches: 1, 354 | pendingWorkCapacity: responseExpected, 355 | responseCallback(queue) { 356 | let responses = queue.splice(0, queue.length); 357 | responses.forEach(({ error, status_code: statusCode }) => { 358 | expect(error.status).toEqual(404); 359 | expect(statusCode).toEqual(404); 360 | responseCount++; 361 | if (responseCount === responseExpected) { 362 | done(); 363 | } 364 | }); 365 | } 366 | }); 367 | 368 | for (let i = 0; i < responseExpected; i++) { 369 | transmission.sendEvent( 370 | new ValidatedEvent({ 371 | apiHost: "http://localhost:9999", 372 | writeKey: "123456789", 373 | dataset: "test-transmission", 374 | sampleRate: 1, 375 | timestamp: new Date(), 376 | postData: { a: 1, b: 2 } 377 | }) 378 | ); 379 | } 380 | }); 381 | 382 | it("should send the right number of events even it requires more batches than maxConcurrentBatch", done => { 383 | let responseCount = 0; 384 | let responseExpected = 50; 385 | let batchSize = 2; 386 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 387 | let reqEvents = JSON.parse(req.body); 388 | let resp = reqEvents.map(() => ({ status: 202 })); 389 | return { text: JSON.stringify(resp) }; 390 | }); 391 | 392 | let transmission = new Transmission({ 393 | batchTimeTrigger: 50, 394 | batchSizeTrigger: batchSize, 395 | pendingWorkCapacity: responseExpected, 396 | responseCallback(queue) { 397 | responseCount += queue.length; 398 | queue.splice(0, queue.length); 399 | if (responseCount === responseExpected) { 400 | done(); 401 | } 402 | } 403 | }); 404 | 405 | for (let i = 0; i < responseExpected; i++) { 406 | transmission.sendEvent( 407 | new ValidatedEvent({ 408 | apiHost: "http://localhost:9999", 409 | writeKey: "123456789", 410 | dataset: "test-transmission", 411 | sampleRate: 1, 412 | timestamp: new Date(), 413 | postData: { a: 1, b: 2 } 414 | }) 415 | ); 416 | } 417 | }); 418 | 419 | it("should send 100% of presampled events", done => { 420 | let responseCount = 0; 421 | let responseExpected = 10; 422 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 423 | let reqEvents = JSON.parse(req.body); 424 | let resp = reqEvents.map(() => ({ status: 202 })); 425 | return { text: JSON.stringify(resp) }; 426 | }); 427 | 428 | let transmission = new Transmission({ 429 | responseCallback(queue) { 430 | let responses = queue.splice(0, queue.length); 431 | responses.forEach(resp => { 432 | if (resp.error) { 433 | console.log(resp.error); 434 | return; 435 | } 436 | responseCount++; 437 | if (responseCount === responseExpected) { 438 | done(); 439 | } 440 | }); 441 | } 442 | }); 443 | 444 | for (let i = 0; i < responseExpected; i++) { 445 | transmission.sendPresampledEvent( 446 | new ValidatedEvent({ 447 | apiHost: "http://localhost:9999", 448 | writeKey: "123456789", 449 | dataset: "test-transmission", 450 | sampleRate: 10, 451 | timestamp: new Date(), 452 | postData: { a: 1, b: 2 } 453 | }) 454 | ); 455 | } 456 | }); 457 | 458 | it("should deal with encoding errors", done => { 459 | let responseCount = 0; 460 | let responseExpected = 11; 461 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 462 | let reqEvents = JSON.parse(req.body); 463 | let resp = reqEvents.map(() => ({ status: 202 })); 464 | return { text: JSON.stringify(resp) }; 465 | }); 466 | 467 | let transmission = new Transmission({ 468 | responseCallback(queue) { 469 | responseCount = queue.length; 470 | return responseCount === responseExpected 471 | ? done() 472 | : done( 473 | Error( 474 | "Incorrect queue length. Queue should equal length of all valid and invalid events enqueued." 475 | ) 476 | ); 477 | } 478 | }); 479 | 480 | for (let i = 0; i < 5; i++) { 481 | transmission.sendPresampledEvent( 482 | new ValidatedEvent({ 483 | apiHost: "http://localhost:9999", 484 | writeKey: "123456789", 485 | dataset: "test-transmission", 486 | sampleRate: 10, 487 | timestamp: new Date(), 488 | postData: { a: 1, b: 2 } 489 | }) 490 | ); 491 | } 492 | { 493 | // send an event that fails to encode 494 | let b = {}; 495 | b.b = b; 496 | transmission.sendPresampledEvent( 497 | new ValidatedEvent({ 498 | apiHost: "http://localhost:9999", 499 | writeKey: "123456789", 500 | dataset: "test-transmission", 501 | sampleRate: 10, 502 | timestamp: new Date(), 503 | postData: b 504 | }) 505 | ); 506 | } 507 | for (let i = 0; i < 5; i++) { 508 | transmission.sendPresampledEvent( 509 | new ValidatedEvent({ 510 | apiHost: "http://localhost:9999", 511 | writeKey: "123456789", 512 | dataset: "test-transmission", 513 | sampleRate: 10, 514 | timestamp: new Date(), 515 | postData: { a: 1, b: 2 } 516 | }) 517 | ); 518 | } 519 | }); 520 | 521 | it("should block on flush", async () => { 522 | let responseCount = 0; 523 | let responseExpected = 50; 524 | let batchSize = 2; 525 | mock.post("http://localhost:9999/1/batch/test-transmission", req => { 526 | let reqEvents = JSON.parse(req.body); 527 | let resp = reqEvents.map(() => ({ status: 202 })); 528 | return { text: JSON.stringify(resp) }; 529 | }); 530 | 531 | let transmission = new Transmission({ 532 | batchTimeTrigger: 50, 533 | batchSizeTrigger: batchSize, 534 | pendingWorkCapacity: responseExpected, 535 | responseCallback(queue) { 536 | responseCount += queue.length; 537 | queue.splice(0, queue.length); 538 | } 539 | }); 540 | 541 | for (let i = 0; i < responseExpected; i++) { 542 | transmission.sendEvent( 543 | new ValidatedEvent({ 544 | apiHost: "http://localhost:9999", 545 | writeKey: "123456789", 546 | dataset: "test-transmission", 547 | sampleRate: 1, 548 | timestamp: new Date(), 549 | postData: { a: 1, b: 2 } 550 | }) 551 | ); 552 | } 553 | 554 | await transmission.flush(); 555 | expect(responseCount).toBe(responseExpected); 556 | }); 557 | 558 | it("should allow user-agent additions", done => { 559 | let responseCount = 0; 560 | let responseExpected = 2; 561 | 562 | let userAgents = [ 563 | { 564 | dataset: "test-transmission1", 565 | addition: "", 566 | probe: userAgent => 567 | // user-agent order: libhoney, node, no addition present 568 | userAgent.indexOf("libhoney-js/<@LIBHONEY_JS_VERSION@>") === 0 && 569 | userAgent.indexOf(`node/${process.version}`) > 1 && 570 | userAgent.indexOf("addition") === -1 571 | }, 572 | { 573 | dataset: "test-transmission2", 574 | addition: "user-agent addition", 575 | probe: userAgent => 576 | // user-agent order: libhoney, addition, node 577 | userAgent.indexOf("libhoney-js/<@LIBHONEY_JS_VERSION@>") === 0 && 578 | userAgent.indexOf("addition") < userAgent.indexOf(`node/${process.version}`) 579 | } 580 | ]; 581 | 582 | // set up our endpoints 583 | userAgents.forEach(userAgent => 584 | mock.post(`http://localhost:9999/1/batch/${userAgent.dataset}`, req => { 585 | if (!userAgent.probe(req.headers["user-agent"])) { 586 | done(new Error("unexpected user-agent addition")); 587 | } 588 | return {}; 589 | }) 590 | ); 591 | 592 | // now send our events through separate transmissions with different user 593 | // agent additions. 594 | userAgents.forEach(userAgent => { 595 | let transmission = new Transmission({ 596 | batchSizeTrigger: 1, // so we'll send individual events 597 | responseCallback(queue) { 598 | let responses = queue.splice(0, queue.length); 599 | responseCount += responses.length; 600 | if (responseCount === responseExpected) { 601 | done(); 602 | } 603 | }, 604 | userAgentAddition: userAgent.addition 605 | }); 606 | 607 | transmission.sendPresampledEvent( 608 | new ValidatedEvent({ 609 | apiHost: "http://localhost:9999", 610 | writeKey: "123456789", 611 | dataset: userAgent.dataset, 612 | sampleRate: 1, 613 | timestamp: new Date(), 614 | postData: { a: 1, b: 2 } 615 | }) 616 | ); 617 | }); 618 | }); 619 | 620 | it("should use X-Honeycomb-UserAgent in browser", done => { 621 | // terrible hack to get our "are we running in node" check to return false 622 | process.env.LIBHONEY_TARGET = "browser"; 623 | 624 | let transmission = new Transmission({ 625 | batchTimeTrigger: 10000, // larger than the mocha timeout 626 | batchSizeTrigger: 0 627 | }); 628 | 629 | mock.post("http://localhost:9999/1/batch/browser-test", req => { 630 | if (req.headers["user-agent"]) { 631 | done(new Error("unexpected user-agent addition")); 632 | } 633 | 634 | if (!req.headers["x-honeycomb-useragent"]) { 635 | done(new Error("missing X-Honeycomb-UserAgent header")); 636 | } 637 | 638 | done(); 639 | 640 | process.env.LIBHONEY_TARGET = ""; 641 | 642 | return {}; 643 | }); 644 | 645 | transmission.sendPresampledEvent( 646 | new ValidatedEvent({ 647 | apiHost: "http://localhost:9999", 648 | writeKey: "123456789", 649 | dataset: "browser-test", 650 | sampleRate: 1, 651 | timestamp: new Date(), 652 | postData: { a: 1, b: 2 } 653 | }) 654 | ); 655 | }); 656 | 657 | it("should respect options.timeout and fail sending the batch", done => { 658 | // we can't use superagent-mocker here, since we want the request to timeout, 659 | // and there's no async flow in -mocker :( 660 | 661 | // This number needs to be less than the global test timeout of 5000 so that the server closes in time 662 | // before jest starts complaining. 663 | const serverTimeout = 2500; // milliseconds 664 | 665 | const server = http.createServer((req, res) => { 666 | setTimeout( 667 | () => { 668 | // this part doesn't really matter 669 | res.writeHead(200, { "Content-Type": "application/json" }); 670 | res.end("[{ status: 666 }]"); 671 | }, 672 | serverTimeout 673 | ); 674 | }); 675 | server.listen(6666, "localhost", () => { 676 | let errResult; 677 | let transmission = new Transmission({ 678 | batchTimeTrigger: 10, 679 | timeout: serverTimeout - 500, 680 | responseCallback: async function (respQueue) { 681 | if (respQueue.length !== 1) { 682 | errResult = new Error(`expected response queue length = 1, got ${respQueue.length}`); 683 | } 684 | 685 | const resp = respQueue[0]; 686 | 687 | if (!(resp.error && resp.error.timeout)) { 688 | errResult = new Error(`expected a timeout error, instead got ${JSON.stringify(resp.error)}`); 689 | } 690 | 691 | server.close(() => { 692 | done(errResult); 693 | }); 694 | } 695 | }); 696 | 697 | transmission.sendEvent( 698 | new ValidatedEvent({ 699 | apiHost: "http://localhost:6666", 700 | writeKey: "123456789", 701 | dataset: "test-transmission", 702 | sampleRate: 1, 703 | timestamp: new Date(), 704 | postData: { a: 1, b: 2 }, 705 | metadata: "my metadata" 706 | }) 707 | ); 708 | }); 709 | }); 710 | }); 711 | -------------------------------------------------------------------------------- /src/builder.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Hound Technology, Inc. All rights reserved. 2 | // Use of this source code is governed by the Apache License 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | /** 6 | * @module 7 | */ 8 | import Event from "./event"; 9 | import foreach from "./foreach"; 10 | 11 | /** 12 | * Allows piecemeal creation of events. 13 | * @class 14 | */ 15 | export default class Builder { 16 | /** 17 | * @constructor 18 | * @private 19 | */ 20 | constructor(libhoney, fields, dynFields) { 21 | this._libhoney = libhoney; 22 | this._fields = Object.create(null); 23 | this._dynFields = Object.create(null); 24 | 25 | /** 26 | * The hostname for the Honeycomb API server to which to send events created through this 27 | * builder. default: https://api.honeycomb.io/ 28 | * 29 | * @type {string} 30 | */ 31 | this.apiHost = ""; 32 | /** 33 | * The Honeycomb authentication token. If it is set on a libhoney instance it will be used as the 34 | * default write key for all events. If absent, it must be explicitly set on a Builder or 35 | * Event. Find your team write key at https://ui.honeycomb.io/account 36 | * 37 | * @type {string} 38 | */ 39 | this.writeKey = ""; 40 | /** 41 | * The name of the Honeycomb dataset to which to send these events. If it is specified during 42 | * libhoney initialization, it will be used as the default dataset for all events. If absent, 43 | * dataset must be explicitly set on a builder or event. 44 | * 45 | * @type {string} 46 | */ 47 | this.dataset = ""; 48 | /** 49 | * The rate at which to sample events. Default is 1, meaning no sampling. If you want to send one 50 | * event out of every 250 times send() is called, you would specify 250 here. 51 | * 52 | * @type {number} 53 | */ 54 | this.sampleRate = 1; 55 | 56 | foreach(fields, (v, k) => this.addField(k, v)); 57 | foreach(dynFields, (v, k) => this.addDynamicField(k, v)); 58 | } 59 | 60 | /** 61 | * adds a group of field->values to the events created from this builder. 62 | * @param {Object|Map} data field->value mapping. 63 | * @returns {Builder} this Builder instance. 64 | * @example using an object 65 | * var honey = new libhoney(); 66 | * var builder = honey.newBuilder(); 67 | * builder.add ({ 68 | * component: "web", 69 | * depth: 200 70 | * }); 71 | * @example using an ES2015 map 72 | * let map = new Map(); 73 | * map.set("component", "web"); 74 | * map.set("depth", 200); 75 | * builder.add (map); 76 | */ 77 | add(data) { 78 | foreach(data, (v, k) => this.addField(k, v)); 79 | return this; 80 | } 81 | 82 | /** 83 | * adds a single field->value mapping to the events created from this builder. 84 | * @param {string} name 85 | * @param {any} val 86 | * @returns {Builder} this Builder instance. 87 | * @example 88 | * builder.addField("component", "web"); 89 | */ 90 | addField(name, val) { 91 | if (val === undefined) { 92 | this._fields[name] = null; 93 | return this; 94 | } 95 | this._fields[name] = val; 96 | return this; 97 | } 98 | 99 | /** 100 | * adds a single field->dynamic value function, which is invoked to supply values when events are created from this builder. 101 | * @param {string} name the name of the field to add to events. 102 | * @param {function(): any} fn the function called to generate the value for this field. 103 | * @returns {Builder} this Builder instance. 104 | * @example 105 | * builder.addDynamicField("process_heapUsed", () => process.memoryUsage().heapUsed); 106 | */ 107 | addDynamicField(name, fn) { 108 | this._dynFields[name] = fn; 109 | return this; 110 | } 111 | 112 | /** 113 | * creates and sends an event, including all builder fields/dynFields, as well as anything in the optional data parameter. 114 | * @param {Object|Map} [data] field->value mapping to add to the event sent. 115 | * @example empty sendNow 116 | * builder.sendNow(); // sends just the data that has been added via add/addField/addDynamicField. 117 | * @example adding data at send-time 118 | * builder.sendNow({ 119 | * additionalField: value 120 | * }); 121 | */ 122 | sendNow(data) { 123 | let ev = this.newEvent(); 124 | ev.add(data); 125 | ev.send(); 126 | } 127 | 128 | /** 129 | * creates and returns a new Event containing all fields/dynFields from this builder, that can be further fleshed out and sent on its own. 130 | * @returns {Event} an Event instance 131 | * @example adding data at send-time 132 | * let ev = builder.newEvent(); 133 | * ev.addField("additionalField", value); 134 | * ev.send(); 135 | */ 136 | newEvent() { 137 | let ev = new Event(this._libhoney, this._fields, this._dynFields); 138 | ev.apiHost = this.apiHost; 139 | ev.writeKey = this.writeKey; 140 | ev.dataset = this.dataset; 141 | ev.sampleRate = this.sampleRate; 142 | return ev; 143 | } 144 | 145 | /** 146 | * creates and returns a clone of this builder, merged with fields and dynFields passed as arguments. 147 | * @param {Object|Map} fields a field->value mapping to merge into the new builder. 148 | * @param {Object|Map} dynFields a field->dynamic function mapping to merge into the new builder. 149 | * @returns {Builder} a Builder instance 150 | * @example no additional fields/dyn_field 151 | * let anotherBuilder = builder.newBuilder(); 152 | * @example additional fields/dyn_field 153 | * let anotherBuilder = builder.newBuilder({ requestId }, 154 | * { 155 | * process_heapUsed: () => process.memoryUsage().heapUsed 156 | * }); 157 | */ 158 | newBuilder(fields, dynFields) { 159 | let b = new Builder(this._libhoney, this._fields, this._dynFields); 160 | 161 | foreach(fields, (v, k) => b.addField(k, v)); 162 | foreach(dynFields, (v, k) => b.addDynamicField(k, v)); 163 | 164 | b.apiHost = this.apiHost; 165 | b.writeKey = this.writeKey; 166 | b.dataset = this.dataset; 167 | b.sampleRate = this.sampleRate; 168 | 169 | return b; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Hound Technology, Inc. All rights reserved. 2 | // Use of this source code is governed by the Apache License 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | /** 6 | * @module 7 | */ 8 | import foreach from "./foreach"; 9 | 10 | /** 11 | * Represents an individual event to send to Honeycomb. 12 | * @class 13 | */ 14 | export default class Event { 15 | /** 16 | * @constructor 17 | * private 18 | */ 19 | constructor(libhoney, fields, dynFields) { 20 | this.data = Object.create(null); 21 | this.metadata = null; 22 | 23 | /** 24 | * The hostname for the Honeycomb API server to which to send this event. default: 25 | * https://api.honeycomb.io/ 26 | * 27 | * @type {string} 28 | */ 29 | this.apiHost = ""; 30 | /** 31 | * The Honeycomb authentication token for this event. Find your team write key at 32 | * https://ui.honeycomb.io/account 33 | * 34 | * @type {string} 35 | */ 36 | this.writeKey = ""; 37 | /** 38 | * The name of the Honeycomb dataset to which to send this event. 39 | * 40 | * @type {string} 41 | */ 42 | this.dataset = ""; 43 | /** 44 | * The rate at which to sample this event. 45 | * 46 | * @type {number} 47 | */ 48 | this.sampleRate = 1; 49 | 50 | /** 51 | * If set, specifies the timestamp associated with this event. If unset, 52 | * defaults to Date.now(); 53 | * 54 | * @type {Date} 55 | */ 56 | this.timestamp = null; 57 | 58 | foreach(fields, (v, k) => this.addField(k, v)); 59 | foreach(dynFields, (v, k) => this.addField(k, v())); 60 | 61 | // stash this away for .send() 62 | this._libhoney = libhoney; 63 | } 64 | 65 | /** 66 | * adds a group of field->values to this event. 67 | * @param {Object|Map} data field->value mapping. 68 | * @returns {Event} this event. 69 | * @example using an object 70 | * builder.newEvent() 71 | * .add ({ 72 | * responseTime_ms: 100, 73 | * httpStatusCode: 200 74 | * }) 75 | * .send(); 76 | * @example using an ES2015 map 77 | * let map = new Map(); 78 | * map.set("responseTime_ms", 100); 79 | * map.set("httpStatusCode", 200); 80 | * let event = honey.newEvent(); 81 | * event.add (map); 82 | * event.send(); 83 | */ 84 | add(data) { 85 | foreach(data, (v, k) => this.addField(k, v)); 86 | return this; 87 | } 88 | 89 | /** 90 | * adds a single field->value mapping to this event. 91 | * @param {string} name 92 | * @param {any} val 93 | * @returns {Event} this event. 94 | * @example 95 | * builder.newEvent() 96 | * .addField("responseTime_ms", 100) 97 | * .send(); 98 | */ 99 | addField(name, val) { 100 | if (val === undefined) { 101 | this.data[name] = null; 102 | return this; 103 | } 104 | this.data[name] = val; 105 | return this; 106 | } 107 | 108 | /** 109 | * attaches data to an event that is not transmitted to honeycomb, but instead is available when checking the send responses. 110 | * @param {any} md 111 | * @returns {Event} this event. 112 | */ 113 | addMetadata(md) { 114 | this.metadata = md; 115 | return this; 116 | } 117 | 118 | /** 119 | * Sends this event to honeycomb, sampling if necessary. 120 | */ 121 | send() { 122 | this._libhoney.sendEvent(this); 123 | } 124 | 125 | /** 126 | * Dispatch an event to be sent to Honeycomb. Assumes sampling has already happened, 127 | * and will send every event handed to it. 128 | */ 129 | sendPresampled() { 130 | this._libhoney.sendPresampledEvent(this); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/foreach.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Hound Technology, Inc. All rights reserved. 2 | // Use of this source code is governed by the Apache License 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | /** 6 | * a simple function that offers the same interface 7 | * for both Map and object key interation. 8 | * @private 9 | */ 10 | export default function foreach(col, f) { 11 | if (!col) { 12 | return; 13 | } 14 | if (col instanceof Map) { 15 | col.forEach(f); 16 | } else { 17 | Object.getOwnPropertyNames(col).forEach(k => f(col[k], k)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/libhoney.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Hound Technology, Inc. All rights reserved. 2 | // Use of this source code is governed by the Apache License 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | // jshint esversion: 6 6 | /** 7 | * @module 8 | */ 9 | import { 10 | ConsoleTransmission, 11 | MockTransmission, 12 | NullTransmission, 13 | StdoutTransmission, 14 | Transmission, 15 | ValidatedEvent, 16 | WriterTransmission, 17 | } from "./transmission"; 18 | import Builder from "./builder"; 19 | 20 | import { EventEmitter } from "events"; 21 | 22 | const classicKeyRegex = /^[a-f0-9]*$/; 23 | const ingestClassicKeyRegex = /^hc[a-z]ic_[a-z0-9]*$/; 24 | 25 | const defaults = Object.freeze({ 26 | apiHost: "https://api.honeycomb.io/", 27 | 28 | // http 29 | proxy: undefined, 30 | 31 | // sample rate of data. causes us to send 1/sample-rate of events 32 | // i.e. `sampleRate: 10` means we only send 1/10th the events. 33 | sampleRate: 1, 34 | 35 | // transmission constructor, or a string to pick one of our builtin versions. 36 | // we fall back to the base impl if worker or a custom implementation throws on init. 37 | // string options available are: 38 | // - "base": the default transmission implementation 39 | // - "worker": a web-worker based transmission (not currently available, see https://github.com/honeycombio/libhoney-js/issues/22) 40 | // - "mock": an implementation that accumulates all events sent 41 | // - "writer": an implementation that logs to the console all events sent (deprecated. use "console" instead) 42 | // - "console": an implementation that logs correct json objects to the console for all events sent. 43 | // - "stdout": an implementation that logs correct json objects to standard out, useful for environments where console.log is not ideal (e.g. AWS Lambda) 44 | // - "null": an implementation that does nothing 45 | transmission: "base", 46 | 47 | // batch triggers 48 | batchSizeTrigger: 50, // we send a batch to the api when we have this many outstanding events 49 | batchTimeTrigger: 100, // ... or after this many ms has passed. 50 | 51 | // batches are sent serially (one event at a time), so we allow multiple concurrent batches 52 | // to increase parallelism while sending. 53 | maxConcurrentBatches: 10, 54 | 55 | // the maximum number of pending events we allow in our to-be-batched-and-transmitted queue before dropping them. 56 | pendingWorkCapacity: 10000, 57 | 58 | // the maximum number of responses we enqueue before we begin dropping them. 59 | maxResponseQueueSize: 1000, 60 | 61 | // how long (in ms) to give a single POST before we timeout. 62 | timeout: 60000, 63 | 64 | // if this is set to true, all sending is disabled. useful for disabling libhoney when testing 65 | disabled: false, 66 | 67 | // If this is non-empty, append it to the end of the User-Agent header. 68 | userAgentAddition: "", 69 | }); 70 | 71 | /** 72 | * libhoney aims to make it as easy as possible to create events and send them on into Honeycomb. 73 | * 74 | * See https://honeycomb.io/docs for background on this library. 75 | * @class 76 | */ 77 | export default class Libhoney extends EventEmitter { 78 | /** 79 | * Constructs a libhoney context in order to configure default behavior, 80 | * though each of its members (`apiHost`, `writeKey`, `dataset`, and 81 | * `sampleRate`) may in fact be overridden on a specific Builder or Event. 82 | * 83 | * @param {Object} [opts] overrides for the defaults 84 | * @param {string} [opts.apiHost=https://api.honeycomb.io] - Server host to receive Honeycomb events. 85 | * @param {string} [opts.proxy] - The proxy to send events through. 86 | * @param {string} opts.writeKey - Write key for your Honeycomb team. (Required) 87 | * @param {string} opts.dataset - Name of the dataset that should contain this event. The dataset will be created for your team if it doesn't already exist. 88 | * @param {number} [opts.sampleRate=1] - Sample rate of data. If set, causes us to send 1/sampleRate of events and drop the rest. 89 | * @param {number} [opts.batchSizeTrigger=50] - We send a batch to the API when this many outstanding events exist in our event queue. 90 | * @param {number} [opts.batchTimeTrigger=100] - We send a batch to the API after this many milliseconds have passed. 91 | * @param {number} [opts.maxConcurrentBatches=10] - We process batches concurrently to increase parallelism while sending. 92 | * @param {number} [opts.pendingWorkCapacity=10000] - The maximum number of pending events we allow to accumulate in our sending queue before dropping them. 93 | * @param {number} [opts.maxResponseQueueSize=1000] - The maximum number of responses we enqueue before dropping them. 94 | * @param {number} [opts.timeout=60000] - How long (in ms) to give a single POST before we timeout. 95 | * @param {boolean} [opts.disabled=false] - Disable transmission of events to the specified `apiHost`, particularly useful for testing or development. 96 | * @constructor 97 | * @example 98 | * import Libhoney from 'libhoney'; 99 | * let honey = new Libhoney({ 100 | * writeKey: "YOUR_WRITE_KEY", 101 | * dataset: "honeycomb-js-example", 102 | * // disabled: true // uncomment when testing or in development 103 | * }); 104 | */ 105 | constructor(opts) { 106 | super(); 107 | this._options = Object.assign( 108 | { responseCallback: this._responseCallback.bind(this) }, 109 | defaults, 110 | opts 111 | ); 112 | this._transmission = getAndInitTransmission( 113 | this._options.transmission, 114 | this._options 115 | ); 116 | this._usable = this._transmission !== null; 117 | this._builder = new Builder(this); 118 | 119 | this._builder.apiHost = this._options.apiHost; 120 | this._builder.writeKey = this._options.writeKey; 121 | this._builder.dataset = this._options.dataset; 122 | this._builder.sampleRate = this._options.sampleRate; 123 | 124 | this._responseQueue = []; 125 | } 126 | 127 | _responseCallback(responses) { 128 | const [queue, limit] = [ 129 | this._responseQueue, 130 | this._options.maxResponseQueueSize, 131 | ]; 132 | 133 | this._responseQueue = concatWithMaxLimit(queue, responses, limit); 134 | 135 | this.emit("response", this._responseQueue); 136 | } 137 | 138 | /** 139 | * The transmission implementation in use for this libhoney instance. Useful when mocking libhoney (specify 140 | * "mock" for options.transmission, and use this field to get at the list of events sent through libhoney.) 141 | */ 142 | get transmission() { 143 | return this._transmission; 144 | } 145 | 146 | /** 147 | * The hostname for the Honeycomb API server to which to send events created through this libhoney 148 | * instance. default: https://api.honeycomb.io/ 149 | * 150 | * @type {string} 151 | */ 152 | set apiHost(v) { 153 | this._builder.apiHost = v; 154 | } 155 | /** 156 | * The hostname for the Honeycomb API server to which to send events created through this libhoney 157 | * instance. default: https://api.honeycomb.io/ 158 | * 159 | * @type {string} 160 | */ 161 | get apiHost() { 162 | return this._builder.apiHost; 163 | } 164 | 165 | /** 166 | * The Honeycomb authentication token. If it is set on a libhoney instance it will be used as the 167 | * default write key for all events. If absent, it must be explicitly set on a Builder or 168 | * Event. Find your team write key at https://ui.honeycomb.io/account 169 | * 170 | * @type {string} 171 | */ 172 | set writeKey(v) { 173 | this._builder.writeKey = v; 174 | } 175 | /** 176 | * The Honeycomb authentication token. If it is set on a libhoney instance it will be used as the 177 | * default write key for all events. If absent, it must be explicitly set on a Builder or 178 | * Event. Find your team write key at https://ui.honeycomb.io/account 179 | * 180 | * @type {string} 181 | */ 182 | get writeKey() { 183 | return this._builder.writeKey; 184 | } 185 | 186 | /** 187 | * The name of the Honeycomb dataset to which to send events through this libhoney instance. If 188 | * it is specified during libhoney initialization, it will be used as the default dataset for all 189 | * events. If absent, dataset must be explicitly set on a builder or event. 190 | * 191 | * @type {string} 192 | */ 193 | set dataset(v) { 194 | this._builder.dataset = v; 195 | } 196 | /** 197 | * The name of the Honeycomb dataset to which to send these events through this libhoney instance. 198 | * If it is specified during libhoney initialization, it will be used as the default dataset for 199 | * all events. If absent, dataset must be explicitly set on a builder or event. 200 | * 201 | * @type {string} 202 | */ 203 | get dataset() { 204 | return this._builder.dataset; 205 | } 206 | 207 | /** 208 | * The rate at which to sample events. Default is 1, meaning no sampling. If you want to send one 209 | * event out of every 250 times send() is called, you would specify 250 here. 210 | * 211 | * @type {number} 212 | */ 213 | set sampleRate(v) { 214 | this._builder.sampleRate = v; 215 | } 216 | /** 217 | * The rate at which to sample events. Default is 1, meaning no sampling. If you want to send one 218 | * event out of every 250 times send() is called, you would specify 250 here. 219 | * 220 | * @type {number} 221 | */ 222 | get sampleRate() { 223 | return this._builder.sampleRate; 224 | } 225 | 226 | /** 227 | * sendEvent takes events of the following form: 228 | * 229 | * { 230 | * data: a JSON-serializable object, keys become colums in Honeycomb 231 | * timestamp [optional]: time for this event, defaults to now() 232 | * writeKey [optional]: your team's write key. overrides the libhoney instance's value. 233 | * dataset [optional]: the data set name. overrides the libhoney instance's value. 234 | * sampleRate [optional]: cause us to send 1 out of sampleRate events. overrides the libhoney instance's value. 235 | * } 236 | * 237 | * Sampling is done based on the supplied sampleRate, so events passed to this method might not 238 | * actually be sent to Honeycomb. 239 | * @private 240 | */ 241 | sendEvent(event) { 242 | let transmitEvent = this.validateEvent(event); 243 | if (!transmitEvent) { 244 | return; 245 | } 246 | 247 | this._transmission.sendEvent(transmitEvent); 248 | } 249 | 250 | /** 251 | * sendPresampledEvent takes events of the following form: 252 | * 253 | * { 254 | * data: a JSON-serializable object, keys become colums in Honeycomb 255 | * timestamp [optional]: time for this event, defaults to now() 256 | * writeKey [optional]: your team's write key. overrides the libhoney instance's value. 257 | * dataset [optional]: the data set name. overrides the libhoney instance's value. 258 | * sampleRate: the rate this event has already been sampled. 259 | * } 260 | * 261 | * Sampling is presumed to have already been done (at the supplied sampledRate), so all events passed to this method 262 | * are sent to Honeycomb. 263 | * @private 264 | */ 265 | sendPresampledEvent(event) { 266 | let transmitEvent = this.validateEvent(event); 267 | if (!transmitEvent) { 268 | return; 269 | } 270 | 271 | this._transmission.sendPresampledEvent(transmitEvent); 272 | } 273 | 274 | /** 275 | * isClassic takes an API key and returns true if it is a "classic" Configuration API Key or Ingest API Key. 276 | * @returns {boolean} whether the key is classic 277 | * 278 | * @example 279 | * if(libhoney.isClassic(apiKey)) { 280 | * // special case for classic environments 281 | * } 282 | */ 283 | static isClassic(key) { 284 | if (key === null || key === undefined || key.length === 0) { 285 | return true; 286 | } 287 | else if(key.length === 32) { 288 | return classicKeyRegex.test(key); 289 | } else if(key.length === 64) { 290 | return ingestClassicKeyRegex.test(key); 291 | } 292 | return false; 293 | } 294 | 295 | /** 296 | * validateEvent takes an event and validates its structure and contents. 297 | * 298 | * @returns {Object} the validated libhoney Event. May return undefined if 299 | * the event was invalid in some way or unable to be sent. 300 | * @private 301 | */ 302 | validateEvent(event) { 303 | if (!this._usable) return null; 304 | 305 | let timestamp = event.timestamp || Date.now(); 306 | if (typeof timestamp === "string" || typeof timestamp === "number") 307 | timestamp = new Date(timestamp); 308 | 309 | if (typeof event.data !== "object" || event.data === null) { 310 | console.error("data must be an object"); 311 | return null; 312 | } 313 | let postData; 314 | try { 315 | postData = JSON.parse(JSON.stringify(event.data)); 316 | } catch (e) { 317 | console.error("error cloning event data: " + e); 318 | return null; 319 | } 320 | 321 | let apiHost = event.apiHost; 322 | if (typeof apiHost !== "string" || apiHost === "") { 323 | console.error("apiHost must be a non-empty string"); 324 | return null; 325 | } 326 | 327 | let writeKey = event.writeKey; 328 | if (typeof writeKey !== "string" || writeKey === "") { 329 | console.error("writeKey must be a non-empty string"); 330 | return null; 331 | } 332 | 333 | let dataset = event.dataset; 334 | if (typeof dataset !== "string") { 335 | console.error("dataset must be a string"); 336 | return null; 337 | } 338 | 339 | if (dataset === "") { 340 | if (Libhoney.isClassic(writeKey)) { 341 | console.error("dataset must be a non-empty string"); 342 | return null; 343 | } else { 344 | dataset = "unknown_dataset"; 345 | } 346 | } 347 | 348 | let sampleRate = event.sampleRate; 349 | if (typeof sampleRate !== "number") { 350 | console.error("sampleRate must be a number"); 351 | return null; 352 | } 353 | 354 | let metadata = event.metadata; 355 | return new ValidatedEvent({ 356 | timestamp, 357 | apiHost, 358 | postData, 359 | writeKey, 360 | dataset, 361 | sampleRate, 362 | metadata, 363 | }); 364 | } 365 | 366 | /** 367 | * adds a group of field->values to the global Builder. 368 | * @param {Object|Map} data field->value mapping. 369 | * @returns {Libhoney} this libhoney instance. 370 | * @example using an object 371 | * honey.add ({ 372 | * buildID: "a6cc38a1", 373 | * env: "staging" 374 | * }); 375 | * @example using an ES2015 map 376 | * let map = new Map(); 377 | * map.set("build_id", "a6cc38a1"); 378 | * map.set("env", "staging"); 379 | * honey.add (map); 380 | */ 381 | add(data) { 382 | this._builder.add(data); 383 | return this; 384 | } 385 | 386 | /** 387 | * adds a single field->value mapping to the global Builder. 388 | * @param {string} name name of field to add. 389 | * @param {any} val value of field to add. 390 | * @returns {Libhoney} this libhoney instance. 391 | * @example 392 | * honey.addField("build_id", "a6cc38a1"); 393 | */ 394 | addField(name, val) { 395 | this._builder.addField(name, val); 396 | return this; 397 | } 398 | 399 | /** 400 | * adds a single field->dynamic value function to the global Builder. 401 | * @param {string} name name of field to add. 402 | * @param {function(): any} fn function that will be called to generate the value whenever an event is created. 403 | * @returns {Libhoney} this libhoney instance. 404 | * @example 405 | * honey.addDynamicField("process_heapUsed", () => process.memoryUsage().heapUsed); 406 | */ 407 | addDynamicField(name, fn) { 408 | this._builder.addDynamicField(name, fn); 409 | return this; 410 | } 411 | 412 | /** 413 | * creates and sends an event, including all global builder fields/dynFields, as well as anything in the optional data parameter. 414 | * @param {Object|Map} data field->value mapping. 415 | * @example using an object 416 | * honey.sendNow ({ 417 | * responseTime_ms: 100, 418 | * httpStatusCode: 200 419 | * }); 420 | * @example using an ES2015 map 421 | * let map = new Map(); 422 | * map.set("responseTime_ms", 100); 423 | * map.set("httpStatusCode", 200); 424 | * honey.sendNow (map); 425 | */ 426 | sendNow(data) { 427 | return this._builder.sendNow(data); 428 | } 429 | 430 | /** 431 | * creates and returns a new Event containing all fields/dynFields from the global Builder, that can be further fleshed out and sent on its own. 432 | * @returns {Event} an Event instance 433 | * @example adding data at send-time 434 | * let ev = honey.newEvent(); 435 | * ev.addField("additionalField", value); 436 | * ev.send(); 437 | */ 438 | newEvent() { 439 | return this._builder.newEvent(); 440 | } 441 | 442 | /** 443 | * creates and returns a clone of the global Builder, merged with fields and dynFields passed as arguments. 444 | * @param {Object|Map} fields a field->value mapping to merge into the new builder. 445 | * @param {Object|Map} dynFields a field->dynamic function mapping to merge into the new builder. 446 | * @returns {Builder} a Builder instance 447 | * @example no additional fields/dyn_field 448 | * let builder = honey.newBuilder(); 449 | * @example additional fields/dyn_field 450 | * let builder = honey.newBuilder({ requestId }, 451 | * { 452 | * process_heapUsed: () => process.memoryUsage().heapUsed 453 | * }); 454 | */ 455 | newBuilder(fields, dynFields) { 456 | return this._builder.newBuilder(fields, dynFields); 457 | } 458 | 459 | /** 460 | * Allows you to easily wait for everything to be sent to Honeycomb (and for responses to come back for 461 | * events). Also initializes a transmission instance for libhoney to use, so any events sent 462 | * after a call to flush will not be waited on. 463 | * @returns {Promise} a promise that will resolve when all currently enqueued events/batches are sent. 464 | */ 465 | flush() { 466 | const transmission = this._transmission; 467 | 468 | this._transmission = getAndInitTransmission( 469 | this._options.transmission, 470 | this._options 471 | ); 472 | 473 | if (!transmission) { 474 | return Promise.resolve(); 475 | } 476 | 477 | return transmission.flush(); 478 | } 479 | } 480 | 481 | const getTransmissionClass = (transmissionClassName) => { 482 | switch (transmissionClassName) { 483 | case "base": 484 | return Transmission; 485 | case "mock": 486 | return MockTransmission; 487 | case "null": 488 | return NullTransmission; 489 | case "worker": 490 | console.warn( 491 | "worker implementation not ready yet. using base implementation" 492 | ); 493 | return Transmission; 494 | case "writer": 495 | console.warn( 496 | "writer implementation is deprecated. Please switch to console implementation." 497 | ); 498 | return WriterTransmission; 499 | case "console": 500 | return ConsoleTransmission; 501 | case "stdout": 502 | return StdoutTransmission; 503 | default: 504 | throw new Error( 505 | `unknown transmission implementation "${transmissionClassName}".` 506 | ); 507 | } 508 | }; 509 | 510 | function getAndInitTransmission(transmission, options) { 511 | if (options.disabled) { 512 | return null; 513 | } 514 | 515 | if (typeof transmission === "string") { 516 | const transmissionClass = getTransmissionClass(transmission); 517 | return new transmissionClass(options); 518 | } else if (typeof transmission !== "function") { 519 | throw new Error( 520 | "transmission must be one of 'base'/'worker'/'mock'/'writer'/'console'/'stdout'/'null' or a constructor." 521 | ); 522 | } 523 | 524 | try { 525 | return new transmission(options); 526 | } catch (initialisationError) { 527 | if (transmission === Transmission) { 528 | throw new Error( 529 | "unable to initialize base transmission implementation.", 530 | initialisationError 531 | ); 532 | } 533 | 534 | console.warn( 535 | "failed to initialize transmission, falling back to base implementation." 536 | ); 537 | try { 538 | return new Transmission(options); 539 | } catch (fallbackInitialisationError) { 540 | throw new Error( 541 | "unable to initialize base transmission implementation.", 542 | fallbackInitialisationError 543 | ); 544 | } 545 | } 546 | } 547 | 548 | /** 549 | * Concatenates two arrays while keeping the length of the returned result 550 | * less than the limit. As many elements from arr2 will be appended onto the 551 | * end of arr1 as will remain under the limit. If arr1 is already too long it 552 | * will be truncated to match the limit. Order is preserved; arr2's contents 553 | * will appear after those already in arr1. 554 | * 555 | * Modifies and returns arr1. 556 | */ 557 | function concatWithMaxLimit(arr1, arr2, limit) { 558 | // if queue is full or somehow over the max 559 | if (arr1.length >= limit) { 560 | //return up to the max length 561 | return arr1.slice(0, limit); 562 | } 563 | 564 | // if queue is not yet full but incoming responses 565 | // would put the queue over 566 | if (arr1.length + arr2.length > limit) { 567 | // find the difference and return only enough responses to fill the queue 568 | const diff = limit - arr1.length; 569 | const slicedArr2 = arr2.slice(0, diff); 570 | return arr1.concat(slicedArr2); 571 | } 572 | 573 | // otherwise assume it'll all fit, combine the responses with the queue 574 | return arr1.concat(arr2); 575 | } 576 | -------------------------------------------------------------------------------- /src/transmission.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Hound Technology, Inc. All rights reserved. 2 | // Use of this source code is governed by the Apache License 2.0 3 | // license that can be found in the LICENSE file. 4 | 5 | /* global global, process */ 6 | 7 | /** 8 | * @module 9 | */ 10 | import superagent from "superagent"; 11 | import urlJoin from "url-join"; 12 | 13 | const LIBHONEY_VERSION = "libhoney-js/<@LIBHONEY_JS_VERSION@>"; 14 | const NODE_VERSION = `node/${process.version}`; 15 | 16 | const _global = 17 | typeof window !== "undefined" 18 | ? window 19 | : typeof global !== "undefined" 20 | ? global 21 | : undefined; 22 | 23 | // how many events to collect in a batch 24 | const batchSizeTrigger = 50; // either when the eventQueue is > this length 25 | const batchTimeTrigger = 100; // or it's been more than this many ms since the first push 26 | 27 | // how many batches to maintain in parallel 28 | const maxConcurrentBatches = 10; 29 | 30 | // how many events to queue up for busy batches before we start dropping 31 | const pendingWorkCapacity = 10000; 32 | 33 | // how long (in ms) to give a single POST before we timeout 34 | const deadlineTimeoutMs = 60000; 35 | 36 | const emptyResponseCallback = function() {}; 37 | 38 | const eachPromise = (arr, iteratorFn) => 39 | arr.reduce((p, item) => { 40 | return p.then(() => { 41 | return iteratorFn(item); 42 | }); 43 | }, Promise.resolve()); 44 | 45 | const partition = (arr, keyfn, createfn, addfn) => { 46 | let result = Object.create(null); 47 | arr.forEach(v => { 48 | let key = keyfn(v); 49 | if (!result[key]) { 50 | result[key] = createfn(v); 51 | } else { 52 | addfn(result[key], v); 53 | } 54 | }); 55 | return result; 56 | }; 57 | 58 | class BatchEndpointAggregator { 59 | constructor(events) { 60 | this.batches = partition( 61 | events, 62 | /* keyfn */ 63 | ev => `${ev.apiHost}_${ev.writeKey}_${ev.dataset}`, 64 | /* createfn */ 65 | ev => ({ 66 | apiHost: ev.apiHost, 67 | writeKey: ev.writeKey, 68 | dataset: ev.dataset, 69 | events: [ev] 70 | }), 71 | /* addfn */ 72 | (batch, ev) => batch.events.push(ev) 73 | ); 74 | } 75 | 76 | encodeBatchEvents(events) { 77 | let first = true; 78 | let numEncoded = 0; 79 | let encodedEvents = events.reduce((acc, ev) => { 80 | try { 81 | let encodedEvent = JSON.stringify(ev); 82 | numEncoded++; 83 | let newAcc = acc + (!first ? "," : "") + encodedEvent; 84 | first = false; 85 | return newAcc; 86 | } catch (e) { 87 | ev.encodeError = e; 88 | return acc; 89 | } 90 | }, ""); 91 | 92 | let encoded = "[" + encodedEvents + "]"; 93 | return { encoded, numEncoded }; 94 | } 95 | } 96 | 97 | /** 98 | * @private 99 | */ 100 | export class ValidatedEvent { 101 | constructor({ 102 | timestamp, 103 | apiHost, 104 | postData, 105 | writeKey, 106 | dataset, 107 | sampleRate, 108 | metadata 109 | }) { 110 | this.timestamp = timestamp; 111 | this.apiHost = apiHost; 112 | this.postData = postData; 113 | this.writeKey = writeKey; 114 | this.dataset = dataset; 115 | this.sampleRate = sampleRate; 116 | this.metadata = metadata; 117 | } 118 | 119 | toJSON() { 120 | let json = {}; 121 | if (this.timestamp) { 122 | json.time = this.timestamp; 123 | } 124 | if (this.sampleRate) { 125 | json.samplerate = this.sampleRate; 126 | } 127 | if (this.postData) { 128 | json.data = this.postData; 129 | } 130 | return json; 131 | } 132 | 133 | /** @deprecated Used by the deprecated WriterTransmission. Use ConsoleTransmission instead. */ 134 | toBrokenJSON() { 135 | let fields = []; 136 | if (this.timestamp) { 137 | fields.push(`"time":${JSON.stringify(this.timestamp)}`); 138 | } 139 | if (this.sampleRate) { 140 | fields.push(`"samplerate":${JSON.stringify(this.sampleRate)}`); 141 | } 142 | if (this.postData) { 143 | fields.push(`"data":${JSON.stringify(this.postData)}`); 144 | } 145 | return `{${fields.join(",")}}`; 146 | } 147 | } 148 | 149 | export class MockTransmission { 150 | constructor(options) { 151 | this.constructorArg = options; 152 | this.events = []; 153 | } 154 | 155 | sendEvent(ev) { 156 | this.events.push(ev); 157 | } 158 | 159 | sendPresampledEvent(ev) { 160 | this.events.push(ev); 161 | } 162 | 163 | reset() { 164 | this.constructorArg = null; 165 | this.events = []; 166 | } 167 | } 168 | 169 | /** @deprecated Use ConsoleTransmission instead. */ 170 | export class WriterTransmission { 171 | sendEvent(ev) { 172 | console.log(JSON.stringify(ev.toBrokenJSON())); 173 | } 174 | 175 | sendPresampledEvent(ev) { 176 | console.log(JSON.stringify(ev.toBrokenJSON())); 177 | } 178 | } 179 | 180 | export class ConsoleTransmission { 181 | sendEvent(ev) { 182 | console.log(JSON.stringify(ev)); 183 | } 184 | 185 | sendPresampledEvent(ev) { 186 | console.log(JSON.stringify(ev)); 187 | } 188 | } 189 | 190 | export class StdoutTransmission { 191 | sendEvent(ev) { 192 | process.stdout.write(JSON.stringify(ev) + "\n"); 193 | } 194 | 195 | sendPresampledEvent(ev) { 196 | process.stdout.write(JSON.stringify(ev) + "\n"); 197 | } 198 | } 199 | 200 | export class NullTransmission { 201 | sendEvent(_ev) {} 202 | 203 | sendPresampledEvent(_ev) {} 204 | } 205 | 206 | /** 207 | * @private 208 | */ 209 | export class Transmission { 210 | constructor(options) { 211 | this._responseCallback = emptyResponseCallback; 212 | this._batchSizeTrigger = batchSizeTrigger; 213 | this._batchTimeTrigger = batchTimeTrigger; 214 | this._maxConcurrentBatches = maxConcurrentBatches; 215 | this._pendingWorkCapacity = pendingWorkCapacity; 216 | this._timeout = deadlineTimeoutMs; 217 | this._sendTimeoutId = -1; 218 | this._eventQueue = []; 219 | this._batchCount = 0; 220 | 221 | if (typeof options.responseCallback === "function") { 222 | this._responseCallback = options.responseCallback; 223 | } 224 | if (typeof options.batchSizeTrigger === "number") { 225 | this._batchSizeTrigger = Math.max(options.batchSizeTrigger, 1); 226 | } 227 | if (typeof options.batchTimeTrigger === "number") { 228 | this._batchTimeTrigger = options.batchTimeTrigger; 229 | } 230 | if (typeof options.maxConcurrentBatches === "number") { 231 | this._maxConcurrentBatches = options.maxConcurrentBatches; 232 | } 233 | if (typeof options.pendingWorkCapacity === "number") { 234 | this._pendingWorkCapacity = options.pendingWorkCapacity; 235 | } 236 | if (typeof options.timeout === "number") { 237 | this._timeout = options.timeout; 238 | } 239 | 240 | this._userAgentAddition = options.userAgentAddition || ""; 241 | this._proxy = options.proxy; 242 | this._proxyAgent = this._determineProxyAgent(this._proxy); 243 | 244 | // Included for testing; to stub out randomness and verify that an event 245 | // was dropped. 246 | this._randomFn = Math.random; 247 | } 248 | 249 | // current proxy config API is only through parameters in code, despite the proxy-agent 250 | // module supporting configuration through the conventional environment variables 251 | // TODO: add config via env vars; which should win over code config and which would need 252 | // to be evaluated per batch API endpoint (complicated) 253 | _determineProxyAgent(proxy) { 254 | // proxy config in code is not supported when running in browsers 255 | if (process.env.LIBHONEY_TARGET === "browser") return undefined; 256 | // no proxy to configure without a proxy URL provided 257 | if (!proxy) return undefined; 258 | 259 | let agentWithProxy = undefined; 260 | 261 | try { 262 | // only import the proxy-agent module after confirming it is needed (e.g. not in a browser) 263 | // eslint-disable-next-line no-undef 264 | const { ProxyAgent } = require("proxy-agent"); 265 | // use the configured proxy URL, regardless of the protocol of the batch API endpoint URL 266 | agentWithProxy = new ProxyAgent({ getProxyForUrl: () => proxy }); 267 | } catch(e) { 268 | console.log(`Unable to configure for transmission through proxy provided: ${proxy}`); 269 | console.log(e); 270 | } 271 | 272 | return agentWithProxy; 273 | } 274 | 275 | _droppedCallback(ev, reason) { 276 | this._responseCallback([ 277 | { 278 | metadata: ev.metadata, 279 | error: new Error(reason) 280 | } 281 | ]); 282 | } 283 | 284 | sendEvent(ev) { 285 | // bail early if we aren't sampling this event 286 | if (!this._shouldSendEvent(ev)) { 287 | this._droppedCallback(ev, "event dropped due to sampling"); 288 | return; 289 | } 290 | 291 | this.sendPresampledEvent(ev); 292 | } 293 | 294 | sendPresampledEvent(ev) { 295 | if (this._eventQueue.length >= this._pendingWorkCapacity) { 296 | this._droppedCallback(ev, "queue overflow"); 297 | return; 298 | } 299 | this._eventQueue.push(ev); 300 | if (this._eventQueue.length >= this._batchSizeTrigger) { 301 | this._sendBatch(); 302 | } else { 303 | this._ensureSendTimeout(); 304 | } 305 | } 306 | 307 | flush() { 308 | if (this._eventQueue.length === 0 && this._batchCount === 0) { 309 | // we're not currently waiting on anything, we're done! 310 | return Promise.resolve(); 311 | } 312 | 313 | return new Promise(resolve => { 314 | this.flushCallback = () => { 315 | this.flushCallback = null; 316 | resolve(); 317 | }; 318 | }); 319 | } 320 | 321 | _sendBatch() { 322 | if (this._batchCount === maxConcurrentBatches) { 323 | // don't start up another concurrent batch. the next timeout/sendEvent or batch completion 324 | // will cause us to send another 325 | return; 326 | } 327 | 328 | this._clearSendTimeout(); 329 | 330 | this._batchCount++; 331 | 332 | let batchAgg = new BatchEndpointAggregator( 333 | this._eventQueue.splice(0, this._batchSizeTrigger) 334 | ); 335 | 336 | const finishBatch = () => { 337 | this._batchCount--; 338 | 339 | let queueLength = this._eventQueue.length; 340 | if (queueLength > 0) { 341 | if (queueLength >= this._batchSizeTrigger) { 342 | this._sendBatch(); 343 | } else { 344 | this._ensureSendTimeout(); 345 | } 346 | return; 347 | } 348 | 349 | if (this._batchCount === 0 && this.flushCallback) { 350 | this.flushCallback(); 351 | } 352 | }; 353 | 354 | let batches = Object.keys(batchAgg.batches).map(k => batchAgg.batches[k]); 355 | eachPromise(batches, batch => { 356 | let url = urlJoin(batch.apiHost, "/1/batch", batch.dataset); 357 | let postReq = superagent.post(url); 358 | 359 | let reqPromise; 360 | if (process.env.LIBHONEY_TARGET === "browser") { 361 | reqPromise = Promise.resolve({ req: postReq }); 362 | } else { 363 | reqPromise = Promise.resolve({ req: postReq.agent(this._proxyAgent) }); 364 | } 365 | let { encoded, numEncoded } = batchAgg.encodeBatchEvents(batch.events); 366 | return reqPromise.then( 367 | ({ req }) => 368 | new Promise(resolve => { 369 | // if we failed to encode any of the events, no point in sending anything to honeycomb 370 | if (numEncoded === 0) { 371 | this._responseCallback( 372 | batch.events.map(ev => ({ 373 | metadata: ev.metadata, 374 | error: ev.encodeError 375 | })) 376 | ); 377 | resolve(); 378 | return; 379 | } 380 | 381 | let userAgent = `${LIBHONEY_VERSION} ${NODE_VERSION}`; 382 | let trimmedAddition = this._userAgentAddition.trim(); 383 | if (trimmedAddition) { 384 | userAgent = `${LIBHONEY_VERSION} ${trimmedAddition} ${NODE_VERSION}`; 385 | } 386 | 387 | let start = Date.now(); 388 | req 389 | .set("X-Honeycomb-Team", batch.writeKey) 390 | .set( 391 | process.env.LIBHONEY_TARGET === "browser" 392 | ? "X-Honeycomb-UserAgent" 393 | : "User-Agent", 394 | userAgent 395 | ) 396 | .type("json") 397 | .timeout(this._timeout) 398 | .send(encoded) 399 | .end((err, res) => { 400 | let end = Date.now(); 401 | 402 | if (err) { 403 | this._responseCallback( 404 | batch.events.map(ev => ({ 405 | // eslint-disable-next-line camelcase 406 | status_code: ev.encodeError ? undefined : err.status, 407 | duration: end - start, 408 | metadata: ev.metadata, 409 | error: ev.encodeError || err 410 | })) 411 | ); 412 | } else { 413 | let response = JSON.parse(res.text); 414 | let respIdx = 0; 415 | this._responseCallback( 416 | batch.events.map(ev => { 417 | if (ev.encodeError) { 418 | return { 419 | duration: end - start, 420 | metadata: ev.metadata, 421 | error: ev.encodeError 422 | }; 423 | } else { 424 | let nextResponse = response[respIdx++]; 425 | return { 426 | // eslint-disable-next-line camelcase 427 | status_code: nextResponse.status, 428 | duration: end - start, 429 | metadata: ev.metadata, 430 | error: nextResponse.err 431 | }; 432 | } 433 | }) 434 | ); 435 | } 436 | // we resolve unconditionally to continue the iteration in eachSeries. errors will cause 437 | // the event to be re-enqueued/dropped. 438 | resolve(); 439 | }); 440 | }) 441 | ); 442 | }) 443 | .then(finishBatch) 444 | .catch(finishBatch); 445 | } 446 | 447 | _shouldSendEvent(ev) { 448 | let { sampleRate } = ev; 449 | if (sampleRate <= 1) { 450 | return true; 451 | } 452 | return this._randomFn() < 1 / sampleRate; 453 | } 454 | 455 | _ensureSendTimeout() { 456 | if (this._sendTimeoutId === -1) { 457 | this._sendTimeoutId = _global.setTimeout( 458 | () => this._sendBatch(), 459 | this._batchTimeTrigger 460 | ); 461 | } 462 | } 463 | 464 | _clearSendTimeout() { 465 | if (this._sendTimeoutId !== -1) { 466 | _global.clearTimeout(this._sendTimeoutId); 467 | this._sendTimeoutId = -1; 468 | } 469 | } 470 | } 471 | --------------------------------------------------------------------------------