├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── jsdoc.json ├── lib ├── api_error.js ├── client.js ├── user_agent.js └── validations.js ├── package.json └── test ├── api_error_test.js ├── client_add_user_properties_test.js ├── client_request_test.js ├── client_test.js ├── client_track_test.js ├── helper.js ├── mocha.opts ├── user_agent_test.js └── validations_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim. 2 | *.swp 3 | 4 | # OSX 5 | .DS_Store 6 | 7 | # Npm modules. 8 | /node_modules 9 | 10 | # Node output. 11 | npm-debug.log 12 | 13 | # Code coverage output. 14 | coverage/ 15 | 16 | # Auto-generated documentation. 17 | doc/ 18 | 19 | # Node packaging output. 20 | heap-*.tgz 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Vim. 2 | *.swp 3 | .vimrc 4 | 5 | # OSX 6 | .DS_Store 7 | 8 | # Git. 9 | .git 10 | 11 | # Npm modules. 12 | /node_modules 13 | 14 | # Node output. 15 | npm-debug.log 16 | 17 | # Code coverage output. 18 | coverage/ 19 | 20 | # Auto-generated documentation. 21 | doc/ 22 | 23 | # Node packaging output. 24 | heap-*.tgz 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | compiler: clang-3.6 4 | env: 5 | - CXX=clang-3.6 6 | addons: 7 | apt: 8 | sources: 9 | - llvm-toolchain-precise-3.6 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - clang-3.6 13 | - g++-4.8 14 | matrix: 15 | include: 16 | - node_js: "0.10" 17 | - node_js: "0.12" 18 | - node_js: "4.0" 19 | - node_js: "4.1" 20 | - node_js: "4.2" 21 | - node_js: "4.3" 22 | - node_js: "4.4" 23 | - node_js: "5" 24 | - node_js: "6.0" 25 | - node_js: "6.1" 26 | - node_js: "6.2" 27 | - node_js: "4.4" 28 | script: npm run coveralls 29 | - node_js: "4.4" 30 | script: npm run doc 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Heap Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _This module is deprecated._ 2 | 3 | # (DEPRECATED) Heap Server-Side API Client for Node.js 4 | 5 | [![Build Status](https://travis-ci.org/heap/heap-node.svg?branch=master)](https://travis-ci.org/heap/heap-node) 6 | [![Coverage Status](https://coveralls.io/repos/github/heap/heap-node/badge.svg?branch=master)](https://coveralls.io/github/heap/heap-node?branch=master) 7 | [![Dependency Status](https://gemnasium.com/heap/heap-node.svg)](https://gemnasium.com/heap/heap-node) 8 | [![NPM Version](http://img.shields.io/npm/v/heap-api.svg)](https://www.npmjs.org/package/heap-api) 9 | 10 | This is a [node.js](https://nodejs.org/) client for the 11 | [Heap](https://heapanalytics.com/) 12 | [server-side API](https://heapanalytics.com/docs/server-side). 13 | 14 | ## Notice 15 | 16 | This client is deprecated and this repo is no longer maintained or supported. 17 | 18 | 19 | ## Prerequisites 20 | 21 | This package is tested on node.js 0.10 and above. 22 | 23 | 24 | ## Installation 25 | 26 | Install using [npm](https://www.npmjs.com/). 27 | 28 | ```bash 29 | npm install heap-api@1.x --save 30 | ``` 31 | 32 | 33 | ## Usage 34 | 35 | Create an API client. 36 | 37 | ```javascript 38 | var heap = require('heap-api')('YOUR_APP_ID'); 39 | ``` 40 | 41 | ### Recommended Usage Patterns 42 | 43 | 44 | [Track](https://heapanalytics.com/docs/server-side#track) a server-side event 45 | in a fire-and-forget fashion. 46 | 47 | ```javascript 48 | heap.track('event-name', 'user-identity'); 49 | heap.track('event-name', 'user-identity', { property: 'value' }); 50 | ``` 51 | 52 | [Add properties](https://heapanalytics.com/docs/server-side#add-user-properties) 53 | to a user. Take advantage of the returned 54 | [ES6 Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 55 | to do more work when the call completes. 56 | 57 | ```javascript 58 | heap.addUserProperties('user-identity', { plan: 'premium1' }) 59 | .then(function() { 60 | // Do more work. 61 | }); 62 | ``` 63 | 64 | Set up an [event](https://nodejs.org/api/events.html) listener to log Heap API 65 | call failures. 66 | 67 | ```javascript 68 | heap.on('error', function(error) { 69 | console.error(error); 70 | }); 71 | ``` 72 | 73 | ### Callback-Based Usage 74 | 75 | Track a server-side event. 76 | 77 | ```javascript 78 | heap.track('event-name', 'user-identity', function(error) { 79 | if (error) 80 | console.error(error); 81 | }); 82 | ``` 83 | 84 | Track a server-side event with properties. 85 | 86 | ```javascript 87 | heap.track('event-name', 'user-identity', { property: 'value' }, function(error) { 88 | if (error) 89 | console.error(error); 90 | }); 91 | ``` 92 | 93 | Add properties to a user. 94 | 95 | ```javascript 96 | heap.addUserProperties('user-identity', { plan: 'premium1' }, function(error) { 97 | if (error) 98 | console.error(error); 99 | }); 100 | ``` 101 | 102 | ### Promise-Based Usage 103 | 104 | The methods described above return 105 | [ES6 Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 106 | The promises can be safely ignored. `track` is a good candidate for 107 | fire-and-forget usage. 108 | 109 | ```javascript 110 | heap.track('event-name', 'user-identity'); 111 | ``` 112 | 113 | Alternatively, the promises can be used to learn when an API call completes or 114 | fails. 115 | 116 | ```javascript 117 | heap.addUserProperties('user-identity', { plan: 'premium1' }) 118 | .then(function() { 119 | console.log("API call succeeded"); 120 | }) 121 | .catch(function(error) { 122 | console.error(error); 123 | }); 124 | ``` 125 | 126 | The Promises are created using 127 | [any-promise](https://www.npmjs.com/package/any-promise), which can be 128 | configured to use your application's favorite Promise implementation. The 129 | [v8 Javascript engine](https://developers.google.com/v8/) versions used by 130 | node.js 0.12 and above include a native Promise implementation that is used by 131 | default. 132 | 133 | For example, the snippet below configures `any-promise` to use 134 | [when.js](https://github.com/cujojs/when). 135 | 136 | ```javascript 137 | require('any-promise/register')('when'); 138 | ``` 139 | 140 | #### Node.js 0.10 141 | 142 | On node.js 0.10 and below, you must either explicitly configure `any-promise` 143 | as shown above, or install a polyfill such as 144 | [es6-promises](https://www.npmjs.com/package/es6-promises). 145 | 146 | ```javascript 147 | require('es6-promises').polyfill(); 148 | ``` 149 | 150 | ### Stubbing 151 | 152 | In some testing environments, connecting to outside servers is undesirable. Set 153 | the `stubbed` property to `true` to have all API calls succeed without 154 | generating any network traffic. 155 | 156 | ```javascript 157 | beforeEach(function() { 158 | heap.stubbed = true; 159 | }); 160 | afterEach(function() { 161 | heap.stubbed = false 162 | }); 163 | ``` 164 | 165 | Alternatively, pass the `stubbed` option when creating the API client. 166 | ```javascript 167 | var heap = require('heap-api')('YOUR_APP_ID', { stubbed: true }); 168 | ``` 169 | 170 | 171 | ## Development 172 | 173 | After cloning the repository, install the dependencies. 174 | 175 | ```bash 176 | npm install 177 | ``` 178 | 179 | Make sure the tests pass after making a change. 180 | 181 | ```bash 182 | npm test 183 | ``` 184 | 185 | When adding new functionality, make sure it has good test coverage. 186 | 187 | ```bash 188 | npm run cov 189 | ``` 190 | 191 | When adding new functionality, also make sure that the documentation looks 192 | reasonable. 193 | 194 | ```bash 195 | npm run doc 196 | ``` 197 | 198 | If you submit a 199 | [pull request](https://help.github.com/articles/using-pull-requests/), 200 | [Travis CI](https://travis-ci.org/) will run the test suite against your code 201 | on the node versions that we support. Please fix any errors that it reports. 202 | 203 | 204 | ## Copyright 205 | 206 | Copyright (c) 2016 Heap Inc., released under the MIT license. 207 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Heap Server-Side API Client. 3 | * 4 | * @module heap-api 5 | */ 6 | 7 | var Client = require("./lib/client.js"); 8 | 9 | /** 10 | * Creates a new client for the Heap server-side API. 11 | * 12 | * @param {string} appId the Heap application ID from 13 | * https://heapanalytics.com/app/install 14 | * @param {Object} options defined below 15 | * @param {boolean} options.stubbed if true, this client makes no Heap API 16 | * calls 17 | * @param {string} options.userAgent the User-Agent header value used by this 18 | * Heap API client 19 | * @returns {heap-api.Client} the new client 20 | */ 21 | module.exports = function(appId, options) { 22 | return new Client(appId, options); 23 | }; 24 | 25 | module.exports.ApiError = require("./lib/api_error.js"); 26 | module.exports.Client = Client; 27 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false, 4 | "dictionaries": ["jsdoc", "closure"] 5 | }, 6 | "source": { 7 | "include": ["./index.js", "./lib"], 8 | "excldude": [], 9 | "includePattern": ".+\\.js(doc)?$", 10 | "excludePattern": "(^|\\/|\\\\)(_|\\.)" 11 | }, 12 | "plugins": [ 13 | "plugins/markdown" 14 | ], 15 | "markdown": { 16 | "parser": "gfm", 17 | "hardwrap": false, 18 | "idInHeadings": true 19 | }, 20 | "templates": { 21 | "logoFile": "", 22 | "systemName": "Heap Server-Side API Client", 23 | "includeDate": false, 24 | "navType": "vertical", 25 | "theme": "yeti", 26 | "inverseNav": false, 27 | "outputSourceFiles": false, 28 | "outputSourcePath": false, 29 | "linenums": true, 30 | "syntaxTheme": "default", 31 | "sort": true, 32 | "methodHeadingReturns": false, 33 | "cleverLinks": false, 34 | "monospaceLinks": false 35 | }, 36 | "opts": { 37 | "template": "./node_modules/ink-docstrap/template", 38 | "encoding": "utf8", 39 | "destination": "./doc/", 40 | "package": "./package.json", 41 | "readme": "./README.md", 42 | "private": true, 43 | "recurse": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/api_error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exception produced when a Heap API call fails. 3 | * 4 | * @module heap-api/api_error 5 | * @private 6 | */ 7 | 8 | var util = require("util"); 9 | 10 | /** 11 | * Creates an exception representing an error sent by the Heap API server. 12 | * 13 | * @constructor 14 | * @param {http.IncomingMessage} response the Heap API server's response 15 | * @param {heap-api.Client} client the Heap API client that received the error 16 | * @alias heap-api.ApiError 17 | */ 18 | function ApiError(response, client) { 19 | this.name = "heap.ApiError"; 20 | this.response = response; 21 | this.client = client; 22 | this.message = this.toString(); 23 | Error.captureStackTrace(this, ApiError); 24 | }; 25 | util.inherits(ApiError, Error); 26 | module.exports = ApiError; 27 | 28 | /** 29 | * Computes this error's message. 30 | * 31 | * This method is defined because V8's exception API uses the exception's 32 | * toString() to compute the first frame of the stack trace. 33 | * 34 | * @private 35 | * @return {string} this exception's message 36 | */ 37 | ApiError.prototype.toString = function() { 38 | if (this.response) { 39 | return "Heap API server error " + this.response.statusCode + ": " + 40 | this.response.body; 41 | } else { 42 | return "Unknown Heap API server error"; 43 | } 44 | }; 45 | 46 | /** 47 | * The response received from the Heap API server. 48 | * @member {?http.IncomingMessage} 49 | */ 50 | ApiError.prototype.response = null; 51 | 52 | /** 53 | * The client that experienced the error. 54 | * @member {heap-api.Client} 55 | */ 56 | ApiError.prototype.client = null; 57 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The class that holds the state of a Heap Server-Side API client. 3 | * 4 | * @module heap-api/client 5 | * @private 6 | */ 7 | 8 | var events = require("events"); 9 | var util = require("util"); 10 | var Promise = require("any-promise"); 11 | var request = require("request"); 12 | 13 | var ApiError = require("./api_error.js"); 14 | var defaultUserAgent = require("./user_agent.js"); 15 | var Validator = require("./validations.js"); 16 | 17 | // NOTE: node.js 0.10 has EventEmitter as a property on events' exported 18 | // object, whereas in newer versions EventEmitter _is_ the exported 19 | // object. 20 | var EventEmitter = ("EventEmitter" in events) ? events.EventEmitter : events; 21 | 22 | 23 | /** 24 | * Creates a new client for the Heap server-side API. 25 | * 26 | * @constructor 27 | * @param {string} appId the Heap application ID from 28 | * https://heapanalytics.com/app/install 29 | * @param {Object} options defined below 30 | * @param {boolean} options.stubbed if true, this client makes no Heap API 31 | * calls 32 | * @param {string} options.userAgent the User-Agent header value used by this 33 | * Heap API client 34 | * @alias heap-api.Client 35 | */ 36 | function Client(appId, options) { 37 | this.appId = appId; 38 | this.stubbed = false; 39 | this.userAgent = defaultUserAgent; 40 | this._validator = new Validator(); 41 | 42 | // NOTE: We need to add a listener to the "error" event so the users aren't 43 | // forced to set up a listener themselves. If no listener for "error" 44 | // exists, emitting the event results in an uncaught error. 45 | this.on("error", function() { }); 46 | 47 | if (options) { 48 | for (var property in options) { 49 | if (!options.hasOwnProperty(property)) 50 | continue; 51 | this[property] = options[property]; 52 | } 53 | } 54 | }; 55 | util.inherits(Client, EventEmitter); 56 | module.exports = Client; 57 | 58 | /** 59 | * Fired when an API call fails. 60 | * 61 | * @event heap-api.Client#error 62 | * @type {heap-api.ApiError} 63 | */ 64 | 65 | 66 | /** 67 | * Assigns custom properties to an existing user. 68 | * 69 | * @param {string} identity an e-mail, handle, or Heap-generated user ID 70 | * @param {Object} properties key-value properties 71 | * associated with the event; each key must have fewer than 1024 characters; 72 | * each value must be a Number or String with fewer than 1024 characters 73 | * @param {?function(Error)} callback called when the API call completes; error 74 | * is undefined if the API call succeeds 75 | * @returns {Promise} resolved with true if the API call completes 76 | * successfully; rejected if the API call fails 77 | * @emits heap-api.Client#error 78 | * @see https://heapanalytics.com/docs/server-side#add-user-properties 79 | */ 80 | Client.prototype.addUserProperties = function(identity, properties, callback) { 81 | options = { 82 | url: "https://heapanalytics.com/api/add_user_properties", 83 | json: { 84 | app_id: this._validator.normalizeAppId(this.appId), 85 | identity: this._validator.normalizeIdentity(identity), 86 | properties: this._validator.normalizeProperties(properties), 87 | }, 88 | }; 89 | 90 | return this._request(options, callback); 91 | }; 92 | 93 | /** 94 | * Sends a custom event to the Heap API servers. 95 | * 96 | * @param {string} eventName the name of the server-side event; limited to 1024 97 | * characters 98 | * @param {string} identity an e-mail, handle, or Heap-generated user ID 99 | * @param {Object} properties key-value properties 100 | * associated with the event; each key must have fewer than 1024 characters; 101 | * each value must be a number or string with fewer than 1024 characters 102 | * @param {?function(Error)} callback called when the API call completes; error 103 | * is undefined if the API call succeeds 104 | * @returns {Promise} resolved with true if the API call completes 105 | * successfully; rejected if the API call fails 106 | * @emits heap-api.Client#error 107 | * @see https://heapanalytics.com/docs/server-side#track 108 | */ 109 | Client.prototype.track = function(eventName, identity, properties, callback) { 110 | options = { 111 | url: "https://heapanalytics.com/api/track", 112 | json: { 113 | app_id: this._validator.normalizeAppId(this.appId), 114 | event: this._validator.normalizeEventName(eventName), 115 | identity: this._validator.normalizeIdentity(identity), 116 | }, 117 | }; 118 | if (typeof(properties) === "function") 119 | callback = properties; 120 | else if (properties) 121 | options.json.properties = this._validator.normalizeProperties(properties); 122 | 123 | return this._request(options, callback); 124 | }; 125 | 126 | /** 127 | * The Heap application ID used by this Heap API client. 128 | * @member {string} 129 | */ 130 | Client.prototype.appId = null; 131 | 132 | /** 133 | * The User-Agent header value used by this Heap API client. 134 | * @member {string} 135 | */ 136 | Client.prototype.userAgent = null; 137 | 138 | /** 139 | * If true, this client makes no Heap API calls. 140 | * 141 | * The client behaves if the API calls have succeeded. 142 | * 143 | * @member {boolean} 144 | */ 145 | Client.prototype.stubbed = null; 146 | 147 | /** 148 | * Heap API parameter validation state. 149 | * 150 | * @private 151 | * @member {heap-api/validations} 152 | */ 153 | Client.prototype._validator = null; 154 | 155 | /** 156 | * Performs a HTTP request to the Heap APIs. 157 | * 158 | * @private 159 | * @param {Object} options the HTTP request arguments; the object can be 160 | * modified by the callee 161 | * @param {?function(Error)} callback called when the API call completes; error 162 | * is undefined if the API call succeeds 163 | * @returns {Promise} resolved with true if the API call completes 164 | * successfully; rejected if the API call fails 165 | * @emits heap-api.Client#error 166 | * @see https://www.npmjs.com/package/request 167 | */ 168 | Client.prototype._request = function(options, callback) { 169 | var validationError = this._validator.error; 170 | if (validationError !== null) { 171 | this._validator.error = null; 172 | this.emit("error", validationError); 173 | if (callback) { 174 | process.nextTick(function() { 175 | callback(validationError); 176 | }); 177 | } 178 | return Promise.reject(validationError); 179 | } 180 | 181 | if (this.stubbed) { 182 | if (callback) 183 | process.nextTick(callback); 184 | return Promise.resolve(true); 185 | } 186 | 187 | options.method = "POST"; 188 | options.headers = { "User-Agent": this.userAgent }; 189 | 190 | var _this = this; 191 | return new Promise(function(resolve, reject) { 192 | request(options, function (error, response) { 193 | if (error) { 194 | response = null; 195 | } else { 196 | if (response.statusCode >= 400) 197 | error = new ApiError(response, _this); 198 | } 199 | 200 | if (error) { 201 | _this.emit("error", error); 202 | reject(error); 203 | callback && callback(error); 204 | return; 205 | } 206 | 207 | resolve(true); 208 | callback && callback(); 209 | }); 210 | }); 211 | }; 212 | -------------------------------------------------------------------------------- /lib/user_agent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The default user-agent value for heap clients. 3 | * 4 | * This is expensive to compute, so we memoize it. 5 | * 6 | * @module heap-api/user_agent 7 | * @private 8 | * @type {string} 9 | */ 10 | 11 | var fs = require("fs"); 12 | 13 | /** 14 | * The version of the heap npm package. 15 | * @private 16 | * @var {string} 17 | */ 18 | var heapVersion = JSON.parse(fs.readFileSync( 19 | require.resolve("../package.json"), { encoding: "utf-8" })).version; 20 | 21 | /** 22 | * The version of the request npm package. 23 | * @private 24 | * @var {string} 25 | */ 26 | var requestVersion = JSON.parse(fs.readFileSync( 27 | require.resolve("request/package.json"), { encoding: "utf-8" })).version; 28 | 29 | module.exports = "heap-node/" + heapVersion + 30 | " request/" + requestVersion + " node/" + process.versions.node + 31 | " (" + process.arch + " " + process.platform + ")" + 32 | " openssl/" + process.versions.openssl; 33 | -------------------------------------------------------------------------------- /lib/validations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Client-side validations for Heap API arguments. 3 | * 4 | * @module heap-api/validations 5 | * @private 6 | */ 7 | 8 | /** 9 | * Creates a context that stores validation errors. 10 | * 11 | * @private 12 | * @constructor 13 | */ 14 | function Validator() { 15 | this.error = null; 16 | }; 17 | 18 | module.exports = Validator; 19 | 20 | /** 21 | * Checks if the argument is a valid Heap API application ID. 22 | * 23 | * @private 24 | * @param {string} appId the application ID to be checked 25 | * @returns {?string} the normalized application ID, which will be passed to 26 | * the Heap API; null if the application ID is not valid 27 | */ 28 | Validator.prototype.normalizeAppId = function(appId) { 29 | if (typeof(appId) !== "string") { 30 | this.error = TypeError("Invalid Heap application ID: " + appId); 31 | return null; 32 | } 33 | if (appId.length === 0) { 34 | this.error = RangeError("Empty Heap application ID"); 35 | return null; 36 | } 37 | return appId; 38 | }; 39 | 40 | /** 41 | * Checks if the argument is not a valid Heap server-side API identity. 42 | * 43 | * @private 44 | * @param {string|number} identity the identity to be checked 45 | * @returns {?string} the normalized identity, which will be passed to the Heap 46 | * API; null if the identity is not valid 47 | */ 48 | Validator.prototype.normalizeIdentity = function(identity) { 49 | if (typeof(identity) === "number") 50 | identity = identity.toString(); 51 | if (typeof(identity) !== "string") { 52 | this.error = TypeError("Invalid identity: " + identity); 53 | return null; 54 | } 55 | if (identity.length === 0) { 56 | this.error = RangeError("Empty identity"); 57 | return null; 58 | } 59 | if (identity.length > 255) { 60 | this.error = RangeError("Identity " + identity + " too long; " + 61 | identity.length + " is above the 255-character limit"); 62 | return null; 63 | } 64 | 65 | return identity; 66 | }; 67 | 68 | /** 69 | * Checks if the argument is not a valid Heap server-side API event name. 70 | * 71 | * @private 72 | * @param {string} eventName the server-side event name to be checked 73 | * @returns {?string} the normalized event name, which will be passed to the 74 | * Heap API; null if the event name is not valid 75 | */ 76 | Validator.prototype.normalizeEventName = function(eventName) { 77 | if (typeof(eventName) !== "string") { 78 | this.error = TypeError("Invalid event name: " + eventName); 79 | return null; 80 | } 81 | if (eventName.length === 0) { 82 | this.error = RangeError("Empty event name"); 83 | return null; 84 | } 85 | if (eventName.length > 1024) { 86 | this.error = RangeError("Event name " + eventName + " too long; " + 87 | eventName.length + " is above the 1024-character limit"); 88 | return null; 89 | } 90 | 91 | return eventName; 92 | }; 93 | 94 | /** 95 | * Checks if the argument is not a valid key-value dictionary for the Heap API. 96 | * 97 | * @private 98 | * @param {Object} properties the key-value dictionary 99 | * to be checked 100 | * @returns {?Object} the normalized key-value 101 | * dictionary, which will be passed to the Heap API; null if the key-value 102 | * dictionary is not valid 103 | */ 104 | Validator.prototype.normalizeProperties = function(properties) { 105 | if (typeof(properties) !== "object" || properties === null) { 106 | this.error = TypeError("Invalid properties: " + properties); 107 | return null; 108 | } 109 | 110 | for (var name in properties) { 111 | if (name.length > 1024) { 112 | this.error = RangeError("Property name " + name + " too long; " + 113 | name.length + " is above the 1024-character limit"); 114 | return null; 115 | } 116 | var value = properties[name]; 117 | 118 | if (typeof(value) === "string") { 119 | if (value.length > 1024) { 120 | this.error = RangeError("Property " + name + " value " + value + 121 | " too long; " + value.length + 122 | " is above the 1024-character limit"); 123 | return null; 124 | } 125 | } else if (typeof(value) !== "number") { 126 | this.error = TypeError("Unsupported type for property " + name + 127 | " value: " + value); 128 | return null; 129 | } 130 | 131 | } 132 | return properties; 133 | }; 134 | 135 | /** 136 | * The last error encountered while checking Heap API arguments. 137 | * 138 | * This attribute should be null between API calls. It is set to non-null 139 | * values by the normalize methods on Validator. It is checked and reset by 140 | * {@link heap-api.Client#_request}. 141 | * 142 | * @member {?Error} 143 | */ 144 | Validator.prototype.error = null; 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heap-api", 3 | "version": "1.0.1", 4 | "description": "Heap Server-Side API Client", 5 | "keywords": ["heap", "api", "client", "analytics"], 6 | "homepage": "https://github.com/heap/heap-node", 7 | "author": "Victor Costan (http://www.costan.us)", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/heap/heap-node.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/heap/heap-node/issues" 15 | }, 16 | "engines": { 17 | "node": ">= 0.10" 18 | }, 19 | "dependencies": { 20 | "any-promise": ">= 0", 21 | "request": ">= 0" 22 | }, 23 | "devDependencies": { 24 | "chai": ">= 3.5.0", 25 | "chai-as-promised": ">= 5.2.0", 26 | "coveralls": ">= 2.11.8", 27 | "es6-promise": ">= 3.1.2", 28 | "ink-docstrap": ">= 1.1.4", 29 | "istanbul": ">= 0.4.2", 30 | "jsdoc": ">= 3.4.0", 31 | "mocha": ">= 2.4.5", 32 | "mocha-lcov-reporter": ">= 1.2.0", 33 | "nock": ">= 7.4.0", 34 | "sinon": ">= 1.17.3", 35 | "sinon-chai": ">= 2.8.0", 36 | "watch": ">= 0.16.0" 37 | }, 38 | "main": "index.js", 39 | "directories": { 40 | "lib": "lib", 41 | "test": "test" 42 | }, 43 | "scripts": { 44 | "cov": "node node_modules/.bin/istanbul cover node_modules/mocha/bin/_mocha", 45 | "coveralls": "node node_modules/.bin/istanbul cover node_modules/mocha/bin/_mocha --report lcovonly && cat ./coverage/lcov.info | node ./node_modules/coveralls/bin/coveralls.js", 46 | "doc": "node node_modules/.bin/jsdoc -c jsdoc.json", 47 | "test": "node node_modules/mocha/bin/mocha" 48 | }, 49 | "config": { 50 | "blanket": { 51 | "pattern": [""], 52 | "data-cover-never": ["node_modules", "test"] 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/api_error_test.js: -------------------------------------------------------------------------------- 1 | var heap = require(".."); 2 | 3 | describe("heap.ApiError", function() { 4 | beforeEach(function() { 5 | this.client = heap("test-app-id"); 6 | this.response = { statusCode: 400, body: "Bad method" }; 7 | this.error = new heap.ApiError(this.response, this.client); 8 | }); 9 | 10 | describe("#constructor", function() { 11 | it("sets the attributes correctly", function() { 12 | expect(this.error.response).to.equal(this.response); 13 | expect(this.error.client).to.equal(this.client); 14 | expect(this.error.name).to.equal("heap.ApiError"); 15 | expect(this.error.message).to.equal( 16 | "Heap API server error 400: Bad method"); 17 | }); 18 | 19 | it("sets the prototype correctly", function() { 20 | expect(this.error).to.be.an.instanceOf(heap.ApiError); 21 | expect(this.error).to.be.an.instanceOf(Error); 22 | }); 23 | 24 | it("sets the stack trace correctly", function() { 25 | expect(this.error.stack).to.match( 26 | /Heap API server error 400: Bad method\n/); 27 | expect(this.error.stack).to.match(/test\/api_error_test\.js:/); 28 | }); 29 | 30 | it("works without a request and a client", function() { 31 | client = new heap.ApiError(); 32 | expect(client).to.be.an.instanceOf(heap.ApiError); 33 | expect(client).to.be.an.instanceOf(Error); 34 | expect(client.message).to.equal("Unknown Heap API server error"); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/client_add_user_properties_test.js: -------------------------------------------------------------------------------- 1 | var nock = require("nock"); 2 | var heap = require(".."); 3 | 4 | describe("heap.Client#addUserProperties", function() { 5 | beforeEach(function() { 6 | this.client = heap("test-app-id"); 7 | }); 8 | 9 | it("errors out when the client has an invalid application ID", function() { 10 | this.client.appId = null; 11 | expect(this.client.addUserProperties(null, { "key": "value" })).to.be. 12 | rejectedWith(TypeError, /^Invalid identity: null$/); 13 | }); 14 | 15 | it("errors out when the identity is invalid", function() { 16 | expect(this.client.addUserProperties(null, { "key": "value" })).to.be. 17 | rejectedWith(TypeError, /^Invalid identity: null$/); 18 | }); 19 | 20 | it("errors out when the properties dictionary is invalid", function() { 21 | expect(this.client.addUserProperties("test-identity", true)).to.be. 22 | rejectedWith(TypeError, /^Invalid properties: true$/); 23 | }); 24 | 25 | describe("with a mock backend", function() { 26 | beforeEach(function() { 27 | this.nock = nock("https://heapanalytics.com"); 28 | nock.disableNetConnect(); 29 | }); 30 | afterEach(function() { 31 | var nockIsDone = nock.isDone(); 32 | if (!nockIsDone) 33 | console.error("Pending HTTP requests: %j", nock.pendingMocks()); 34 | nock.enableNetConnect(); 35 | nock.cleanAll(); 36 | if (!nockIsDone) 37 | throw Error("Test failed to issue all expected HTTP requests"); 38 | }); 39 | 40 | it("works with string identities", function() { 41 | this.nock.post("/api/add_user_properties").reply(204, 42 | function(uri, requestBody) { 43 | expect(requestBody).to.deep.equal({ 44 | app_id: "test-app-id", identity: "test-identity", 45 | properties: { foo: "bar" }, 46 | }); 47 | }); 48 | 49 | return expect(this.client.addUserProperties("test-identity", 50 | { foo: "bar" })).to.become(true); 51 | }); 52 | 53 | it("works with integer identities", function() { 54 | this.nock.post("/api/add_user_properties").reply(204, 55 | function(uri, requestBody) { 56 | expect(requestBody).to.deep.equal({ 57 | app_id: "test-app-id", identity: "123456789", 58 | properties: { foo: "bar" }, 59 | }); 60 | }); 61 | 62 | return expect(this.client.addUserProperties(123456789, { foo: "bar" })). 63 | to.become(true); 64 | }); 65 | 66 | it("works with a callback", function(done) { 67 | this.nock.post("/api/add_user_properties").reply(204, 68 | function(uri, requestBody) { 69 | expect(requestBody).to.deep.equal({ 70 | app_id: "test-app-id", identity: "test-identity", 71 | properties: { foo: "bar" }, 72 | }); 73 | }); 74 | 75 | this.client.addUserProperties("test-identity", { foo: "bar" }, 76 | function(error) { 77 | expect(error).to.be.undefined; 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | describe("with a stubbed client", function() { 84 | beforeEach(function() { 85 | this.client.stubbed = true; 86 | }); 87 | 88 | it("succeeds", function() { 89 | return expect(this.client.addUserProperties("test-identity", 90 | { foo: "bar" })).to.become(true); 91 | }); 92 | }); 93 | 94 | describe("when talking to the real backend", function() { 95 | beforeEach(function() { 96 | this.client.appId = "3000610572"; 97 | }); 98 | 99 | it("succeeds", function() { 100 | return expect(this.client.addUserProperties("test-identity", 101 | { "language/node": "1", "heap/heap-node": 1 })).to.become(true); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/client_request_test.js: -------------------------------------------------------------------------------- 1 | var nock = require("nock"); 2 | var heap = require(".."); 3 | 4 | describe("heap.Client#_request", function() { 5 | beforeEach(function() { 6 | this.client = heap("test-app-id"); 7 | this.requestOptions = { 8 | url: "https://heapanalytics.com/api/add_user_properties", 9 | json: { 10 | app_id: "test-app-id", 11 | identity: "test-identity", 12 | properties: { "language/node": "1" }, 13 | } 14 | }; 15 | }); 16 | 17 | describe("with a mock backend", function() { 18 | beforeEach(function() { 19 | this.nock = nock("https://heapanalytics.com"); 20 | nock.disableNetConnect(); 21 | }); 22 | afterEach(function() { 23 | var nockIsDone = nock.isDone(); 24 | if (!nockIsDone) 25 | console.error("Pending HTTP requests: %j", nock.pendingMocks()); 26 | nock.enableNetConnect(); 27 | nock.cleanAll(); 28 | if (!nockIsDone) 29 | throw Error("Test failed to issue all expected HTTP requests"); 30 | }); 31 | 32 | it("encodes the JSON payload correctly", function() { 33 | this.nock.post("/api/add_user_properties").reply(204, 34 | function(uri, requestBody) { 35 | expect(this.req.headers["content-type"]).to.equal("application/json"); 36 | expect(requestBody).to.deep.equal({ 37 | app_id: "test-app-id", identity: "test-identity", 38 | properties: { "language/node": "1" }, 39 | }); 40 | }); 41 | 42 | return expect(this.client._request(this.requestOptions)).to.become(true); 43 | }); 44 | 45 | it("sets the user-agent string correctly", function() { 46 | this.client.userAgent = "test/user/agent"; 47 | this.nock.post("/api/add_user_properties").reply(204, 48 | function(uri, requestBody) { 49 | expect(this.req.headers["user-agent"]).to.equal("test/user/agent"); 50 | }); 51 | 52 | 53 | return expect(this.client._request(this.requestOptions)).to.become(true); 54 | }); 55 | 56 | it("calls the callback if the API call succeeds", function(done) { 57 | this.nock.post("/api/add_user_properties").reply(204, function() {}); 58 | this.client._request(this.requestOptions, function(error) { 59 | expect(error).to.be.undefined; 60 | done(); 61 | }); 62 | }); 63 | 64 | it("reports server errors to callback", function(done) { 65 | this.nock.post("/api/add_user_properties").reply(400, "Bad request"); 66 | 67 | this.client._request(this.requestOptions, function(error) { 68 | expect(error).to.be.an.instanceOf(heap.ApiError); 69 | expect(error.message).to.equal( 70 | "Heap API server error 400: Bad request"); 71 | done(); 72 | }); 73 | }); 74 | 75 | it("reports request errors to callback", function(done) { 76 | this.nock.post("/api/add_user_properties"). 77 | replyWithError("Mock request error"); 78 | 79 | this.client._request(this.requestOptions, function(error) { 80 | expect(error).to.be.an.instanceOf(Error); 81 | expect(error.message).to.equal("Mock request error"); 82 | done(); 83 | }); 84 | }); 85 | 86 | it("reports pending validation errors to callback", function(done) { 87 | var validationError = new Object(); 88 | this.client._validator.error = validationError; 89 | 90 | this.client._request(this.requestOptions, function(error) { 91 | expect(error).to.equal(validationError); 92 | done(); 93 | }); 94 | }); 95 | 96 | it("rejects the returned promise on server errors", function() { 97 | this.nock.post("/api/add_user_properties").reply(400, "Bad request"); 98 | 99 | return expect(this.client._request(this.requestOptions)).to.be. 100 | rejectedWith(heap.ApiError, 101 | /^Heap API server error 400: Bad request$/); 102 | }); 103 | 104 | it("rejects the returned promise on request errors", function() { 105 | this.nock.post("/api/add_user_properties"). 106 | replyWithError("Mock request error"); 107 | 108 | return expect(this.client._request(this.requestOptions)).to.eventually. 109 | be.rejectedWith(Error, /^Error: Mock request error$/); 110 | }); 111 | 112 | it("rejects the returned promise on pending validation errors", 113 | function() { 114 | var validationError = new Object(); 115 | this.client._validator.error = validationError; 116 | 117 | return expect(this.client._request(this.requestOptions)).to.eventually. 118 | be.rejectedWith(validationError); 119 | }); 120 | 121 | it("issues events for server errors", function(done) { 122 | this.nock.post("/api/add_user_properties").reply(400, "Bad request"); 123 | 124 | var client = this.client; 125 | client.on("error", function(error) { 126 | expect(error).to.be.an.instanceOf(heap.ApiError); 127 | expect(error.message).to.equal( 128 | "Heap API server error 400: Bad request"); 129 | expect(error.response).to.have.property("statusCode", 400); 130 | expect(error.response).to.have.property("body", "Bad request"); 131 | expect(error.client).to.equal(client); 132 | done(); 133 | }); 134 | client._request(this.requestOptions); 135 | }); 136 | 137 | it("issues events for request errors", function(done) { 138 | this.nock.post("/api/add_user_properties"). 139 | replyWithError("Mock request error"); 140 | 141 | var client = this.client; 142 | client.on("error", function(error) { 143 | expect(error).to.be.an.instanceOf(Error); 144 | expect(error.message).to.equal("Mock request error"); 145 | done(); 146 | }); 147 | client._request(this.requestOptions); 148 | }); 149 | 150 | it("issues events for pending validation errors", function(done) { 151 | var validationError = new Object(); 152 | this.client._validator.error = validationError; 153 | 154 | var client = this.client; 155 | client.on("error", function(error) { 156 | expect(error).to.equal(validationError); 157 | done(); 158 | }); 159 | client._request(this.requestOptions); 160 | }); 161 | 162 | it("calls the callback before the promise is resolved", function(done) { 163 | this.nock.post("/api/add_user_properties").reply(204, function() {}); 164 | 165 | var promiseResolved = false; 166 | var callbackCalled = false; 167 | this.client._request(this.requestOptions, function(error) { 168 | expect(error).to.be.undefined; 169 | expect(promiseResolved).to.equal(false); 170 | expect(callbackCalled).to.equal(false); 171 | callbackCalled = true; 172 | }).then(function() { 173 | expect(promiseResolved).to.equal(false); 174 | promiseResolved = true; 175 | expect(callbackCalled).to.equal(true); 176 | done(); 177 | }); 178 | }); 179 | 180 | it("calls the callback before the promise is rejected", function(done) { 181 | this.nock.post("/api/add_user_properties").reply(400, "Bad request"); 182 | 183 | var promiseRejected = false; 184 | var callbackCalled = false; 185 | this.client._request(this.requestOptions, function(error) { 186 | expect(error).to.be.an.instanceOf(heap.ApiError); 187 | expect(error.message).to.equal( 188 | "Heap API server error 400: Bad request"); 189 | expect(promiseRejected).to.equal(false); 190 | expect(callbackCalled).to.equal(false); 191 | callbackCalled = true; 192 | }).catch(function(error) { 193 | expect(error).to.be.an.instanceOf(heap.ApiError); 194 | expect(error.message).to.equal( 195 | "Heap API server error 400: Bad request"); 196 | expect(promiseRejected).to.equal(false); 197 | promiseRejected = true; 198 | expect(callbackCalled).to.equal(true); 199 | done(); 200 | }); 201 | }); 202 | 203 | it("calls the callback before the promise is rejected due to validation", 204 | function(done) { 205 | var validationError = new Object(); 206 | this.client._validator.error = validationError; 207 | 208 | var promiseRejected = false; 209 | var callbackCalled = false; 210 | this.client._request(this.requestOptions, function(error) { 211 | expect(error).to.equal(validationError); 212 | expect(promiseRejected).to.equal(false); 213 | expect(callbackCalled).to.equal(false); 214 | callbackCalled = true; 215 | }).catch(function(error) { 216 | expect(error).to.equal(validationError); 217 | expect(promiseRejected).to.equal(false); 218 | promiseRejected = true; 219 | expect(callbackCalled).to.equal(true); 220 | done(); 221 | }); 222 | }); 223 | 224 | it("issues the error event before calling the callback", function(done) { 225 | this.nock.post("/api/add_user_properties").reply(400, "Bad request"); 226 | 227 | var eventIssued = false; 228 | var callbackCalled = false; 229 | this.client.on("error", function(error) { 230 | expect(error).to.be.an.instanceOf(heap.ApiError); 231 | expect(error.message).to.equal( 232 | "Heap API server error 400: Bad request"); 233 | expect(eventIssued).to.equal(false); 234 | eventIssued = true; 235 | expect(callbackCalled).to.equal(false); 236 | done(); 237 | }); 238 | this.client._request(this.requestOptions, function(error) { 239 | expect(error).to.be.an.instanceOf(Error); 240 | expect(error.message).to.equal( 241 | "Heap API server error 400: Bad request"); 242 | expect(eventIssued).to.equal(true); 243 | expect(callbackCalled).to.equal(false); 244 | callbackCalled = true; 245 | }); 246 | }); 247 | 248 | it("issues the validation error event before calling the callback", 249 | function(done) { 250 | var validationError = new Object(); 251 | this.client._validator.error = validationError; 252 | 253 | var eventIssued = false; 254 | var callbackCalled = false; 255 | this.client.on("error", function(error) { 256 | expect(error).to.equal(validationError); 257 | expect(eventIssued).to.equal(false); 258 | eventIssued = true; 259 | expect(callbackCalled).to.equal(false); 260 | done(); 261 | }); 262 | this.client._request(this.requestOptions, function(error) { 263 | expect(error).to.equal(validationError); 264 | expect(eventIssued).to.equal(true); 265 | expect(callbackCalled).to.equal(false); 266 | callbackCalled = true; 267 | }); 268 | }); 269 | }); 270 | 271 | describe("with a stubbed client", function() { 272 | beforeEach(function() { 273 | this.client.stubbed = true; 274 | }); 275 | 276 | it("calls the callback, when provided", function(done) { 277 | this.client._request(this.requestOptions, function (error) { 278 | expect(error).to.be.undefined; 279 | done(); 280 | }); 281 | }); 282 | }); 283 | 284 | describe("when talking to the real backend", function() { 285 | beforeEach(function() { 286 | this.requestOptions.json.app_id = this.client.appId = "3000610572"; 287 | }); 288 | 289 | it("succeeds", function() { 290 | return expect(this.client._request(this.requestOptions)).to.become(true); 291 | }); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /test/client_test.js: -------------------------------------------------------------------------------- 1 | var heap = require(".."); 2 | 3 | describe("heap.Client", function() { 4 | beforeEach(function() { 5 | this.client = heap("test-app-id"); 6 | }); 7 | 8 | describe("#appId", function() { 9 | it("gets the constructor argument value", function() { 10 | expect(this.client.appId).to.equal("test-app-id"); 11 | }); 12 | }); 13 | 14 | describe("#stubbed", function() { 15 | it("is false by default", function() { 16 | expect(this.client.stubbed).to.equal(false); 17 | }); 18 | it("gets the constructor option value", function() { 19 | var client = heap("test-app-id", { stubbed: true }); 20 | 21 | expect(client.stubbed).to.equal(true); 22 | expect(client.appId).to.equal("test-app-id"); 23 | }); 24 | }); 25 | 26 | describe("#userAgent", function() { 27 | it("is heap.user_agent by default", function() { 28 | expect(this.client.userAgent).to.equal(require("../lib/user_agent.js")); 29 | }); 30 | 31 | it("gets the constructor option value", function() { 32 | var client = heap("test-app-id", { userAgent: "test-user-agent" }); 33 | 34 | expect(client.userAgent).to.equal("test-user-agent"); 35 | expect(client.appId).to.equal("test-app-id"); 36 | }); 37 | }); 38 | 39 | describe("#constructor", function() { 40 | it("ignores non-own properties on options object", function() { 41 | var optionsPrototype = { protoProperty: true }; 42 | var Options = function() { return this; }; 43 | Options.prototype = optionsPrototype; 44 | options = new Options(); 45 | 46 | options.userAgent = "test-user-agent"; 47 | var client = heap("test-app-id", options); 48 | expect(client).not.to.have.property("protoProperty"); 49 | expect(client.userAgent).to.equal("test-user-agent"); 50 | expect(client.appId).to.equal("test-app-id"); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/client_track_test.js: -------------------------------------------------------------------------------- 1 | var nock = require("nock"); 2 | var heap = require(".."); 3 | 4 | describe("heap.Client#track", function() { 5 | beforeEach(function() { 6 | this.client = heap("test-app-id"); 7 | }); 8 | 9 | it("errors out when the client has an invalid application ID", function() { 10 | this.client.appId = null; 11 | expect(this.client.track("test_track_with_invalid_app_id", "test-identity", 12 | { "key": "value" })).to.be.rejectedWith(TypeError, 13 | /^Invalid Heap application ID: null$/); 14 | }); 15 | 16 | it("errors out when the event name is invalid", function() { 17 | expect(this.client.track(null, "test-identity", { "key": "value" })).to.be. 18 | rejectedWith(Error, /^Invalid event name: null$/); 19 | }); 20 | 21 | it("errors out when the identity is invalid", function() { 22 | expect(this.client.track("test_track_with_invalid_identity", null, 23 | { "key": "value" })).to.be.rejectedWith(TypeError, 24 | /^Invalid identity: null$/); 25 | }); 26 | 27 | it("errors out when the properties dictionary is invalid", function() { 28 | expect(this.client.track("test_track_with_invalid_properties", 29 | "test-identity", true)).to.be.rejectedWith(Error, 30 | /^Invalid properties: true$/); 31 | }); 32 | 33 | describe("with a mock backend", function() { 34 | beforeEach(function() { 35 | this.nock = nock("https://heapanalytics.com"); 36 | nock.disableNetConnect(); 37 | }); 38 | afterEach(function() { 39 | var nockIsDone = nock.isDone(); 40 | if (!nockIsDone) 41 | console.error("Pending HTTP requests: %j", nock.pendingMocks()); 42 | nock.enableNetConnect(); 43 | nock.cleanAll(); 44 | if (!nockIsDone) 45 | throw Error("Test failed to issue all expected HTTP requests"); 46 | }); 47 | 48 | it("works with string identities", function() { 49 | this.nock.post("/api/track").reply(204, function(uri, requestBody) { 50 | expect(requestBody).to.deep.equal({ 51 | app_id: "test-app-id", event: "test_track_with_string_identity", 52 | identity: "test-identity", properties: { foo: "bar" }, 53 | }); 54 | }); 55 | 56 | return expect(this.client.track("test_track_with_string_identity", 57 | "test-identity", { foo: "bar" })).to.become(true); 58 | }); 59 | 60 | it("works with integer identities", function() { 61 | this.nock.post("/api/track").reply(204, function(uri, requestBody) { 62 | expect(requestBody).to.deep.equal({ 63 | app_id: "test-app-id", event: "test_track_with_integer_identity", 64 | identity: "123456789", properties: { foo: "bar" }, 65 | }); 66 | }); 67 | 68 | return expect(this.client.track("test_track_with_integer_identity", 69 | 123456789, { foo: "bar" })).to.become(true); 70 | }); 71 | 72 | it("works with null properties", function() { 73 | this.nock.post("/api/track").reply(204, function(uri, requestBody) { 74 | expect(requestBody).to.deep.equal({ 75 | app_id: "test-app-id", event: "test_track_with_null_properties", 76 | identity: "test-identity", 77 | }); 78 | }); 79 | 80 | return expect(this.client.track("test_track_with_null_properties", 81 | "test-identity", null)).to.become(true); 82 | }); 83 | 84 | it("works without properties", function() { 85 | this.nock.post("/api/track").reply(204, function(uri, requestBody) { 86 | expect(requestBody).to.deep.equal({ 87 | app_id: "test-app-id", event: "test_track_without_properties", 88 | identity: "test-identity", 89 | }); 90 | }); 91 | 92 | return expect(this.client.track("test_track_without_properties", 93 | "test-identity")).to.become(true); 94 | }); 95 | 96 | it("works with a callback without properties", function(done) { 97 | this.nock.post("/api/track").reply(204, function(uri, requestBody) { 98 | expect(requestBody).to.deep.equal({ 99 | app_id: "test-app-id", event: "test_track_with_callback", 100 | identity: "test-identity", 101 | }); 102 | }); 103 | 104 | this.client.track("test_track_with_callback", "test-identity", 105 | function(error) { 106 | expect(error).to.be.undefined; 107 | done(); 108 | }); 109 | }); 110 | }); 111 | 112 | describe("with a stubbed client", function() { 113 | beforeEach(function() { 114 | this.client.stubbed = true; 115 | }); 116 | 117 | it("succeeds", function() { 118 | return expect(this.client.track("test_track_with_stubbed_client", 119 | "test-identity", { foo: "bar" })).to.become(true); 120 | }); 121 | }); 122 | 123 | describe("when talking to the real backend", function() { 124 | beforeEach(function() { 125 | this.client.appId = "3000610572"; 126 | }); 127 | 128 | it("succeeds", function() { 129 | return expect(this.client.track("test_track_integration", 130 | "test-identity", { language: "node", project: "heap/heap-node" })). 131 | to.become(true); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | // node.js 0.10 lacks a native Promise implementation, so we use a polyfill. 2 | require("es6-promise").polyfill(); 3 | 4 | global.chai = require("chai"); 5 | global.sinon = require("sinon"); 6 | global.chai.use(require("chai-as-promised")); 7 | global.chai.use(require("sinon-chai")); 8 | 9 | global.assert = global.chai.assert; 10 | global.expect = global.chai.expect; 11 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/helper.js 2 | --timeout 30000 3 | --ui bdd 4 | -------------------------------------------------------------------------------- /test/user_agent_test.js: -------------------------------------------------------------------------------- 1 | var userAgent = require("../lib/user_agent.js"); 2 | 3 | describe("heap/user_agent", function() { 4 | it("contains heap-node version", function() { 5 | expect(userAgent).to.match(/^heap-node\/[\d.]+ /); 6 | }); 7 | it("contains request version", function() { 8 | expect(userAgent).to.match(/ request\/[\d.]+ /); 9 | }); 10 | it("contains node version, architecture and platform", function() { 11 | expect(userAgent).to.match(/ node\/[\d.]+ \(.+ .+\) /); 12 | }); 13 | it("contains openssl version", function() { 14 | expect(userAgent).to.match(/ openssl\/\S+$/); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/validations_test.js: -------------------------------------------------------------------------------- 1 | var Validator = require("../lib/validations.js"); 2 | 3 | describe("heap/validations~Validator", function() { 4 | beforeEach(function() { 5 | this.validator = new Validator(); 6 | }); 7 | describe("#constructor", function() { 8 | it("initializes #error to null", function() { 9 | expect(this.validator).to.have.property("error", null); 10 | }); 11 | }); 12 | describe("#normalizeAppId", function() { 13 | it("rejects non-strings", function() { 14 | var values = [null, 0, 1, 42, true, false, undefined, [], {}, 15 | function() {}]; 16 | for (var i = 0; i < values.length; ++i) { 17 | var value = values[0]; 18 | expect(this.validator.normalizeAppId(value)).to.equal(null); 19 | expect(this.validator.error).to.be.an.instanceOf(TypeError); 20 | expect(this.validator.error.message).to.equal( 21 | "Invalid Heap application ID: " + value); 22 | } 23 | }); 24 | 25 | it("rejects empty strings", function() { 26 | expect(this.validator.normalizeAppId("")).to.equal(null); 27 | expect(this.validator.error).to.be.an.instanceOf(RangeError); 28 | expect(this.validator.error.message).to.equal( 29 | "Empty Heap application ID"); 30 | }); 31 | 32 | it("returns the argument", function() { 33 | expect(this.validator.normalizeAppId("test-app-id")).to. 34 | equal("test-app-id"); 35 | expect(this.validator.error).to.equal(null); 36 | }); 37 | }); 38 | 39 | describe("#normalizeIdentity", function() { 40 | it("rejects invalid types", function() { 41 | var values = [null, true, false, undefined, [], {}, function() {}]; 42 | for (var i = 0; i < values.length; ++i) { 43 | var value = values[0]; 44 | expect(this.validator.normalizeIdentity(value)).to.equal(null); 45 | expect(this.validator.error).to.be.an.instanceOf(TypeError); 46 | expect(this.validator.error.message).to.equal( 47 | "Invalid identity: " + value); 48 | } 49 | }); 50 | 51 | it("rejects empty strings", function() { 52 | expect(this.validator.normalizeIdentity("")).to.equal(null); 53 | expect(this.validator.error).to.be.an.instanceOf(RangeError); 54 | expect(this.validator.error.message).to.equal( 55 | "Empty identity"); 56 | }); 57 | 58 | it("rejects long strings", function() { 59 | longIdentity = new Array(257).join("A") 60 | expect(this.validator.normalizeIdentity(longIdentity)).to.equal(null); 61 | expect(this.validator.error).to.be.an.instanceOf(RangeError); 62 | expect(this.validator.error.message).to.equal("Identity " + 63 | longIdentity + " too long; 256 is " + 64 | "above the 255-character limit"); 65 | }); 66 | 67 | 68 | it("returns a string argument", function() { 69 | expect(this.validator.normalizeIdentity("test-identity")).to.equal( 70 | "test-identity"); 71 | expect(this.validator.error).to.equal(null); 72 | }); 73 | 74 | it("stringifies a number argument", function() { 75 | expect(this.validator.normalizeIdentity(42)).to.equal("42"); 76 | expect(this.validator.error).to.equal(null); 77 | }); 78 | }); 79 | 80 | describe("#normalizeEventName", function() { 81 | it("rejects invalid types", function() { 82 | var values = [null, 0, 1, 42, true, false, undefined, [], {}, 83 | function() {}]; 84 | for (var i = 0; i < values.length; ++i) { 85 | var value = values[0]; 86 | expect(this.validator.normalizeEventName(value)).to.equal(null); 87 | expect(this.validator.error).to.be.an.instanceOf(TypeError); 88 | expect(this.validator.error.message).to.equal( 89 | "Invalid event name: " + value); 90 | } 91 | }); 92 | 93 | it("rejects empty strings", function() { 94 | expect(this.validator.normalizeEventName("")).to.equal(null); 95 | expect(this.validator.error).to.be.an.instanceOf(RangeError); 96 | expect(this.validator.error.message).to.equal("Empty event name"); 97 | }); 98 | 99 | it("rejects long strings", function() { 100 | longEventName = new Array(1026).join("A") 101 | expect(this.validator.normalizeEventName(longEventName)).to.equal(null); 102 | expect(this.validator.error).to.be.an.instanceOf(RangeError); 103 | expect(this.validator.error.message).to.equal("Event name " + 104 | longEventName + " too long; 1025 is above the 1024-character limit"); 105 | }); 106 | 107 | it("returns the argument", function() { 108 | expect(this.validator.normalizeEventName("test-event")).to.equal( 109 | "test-event"); 110 | expect(this.validator.error).to.equal(null); 111 | }); 112 | }); 113 | 114 | 115 | describe("#normalizeProperties", function() { 116 | it("rejects invalid types", function() { 117 | var values = [null, 0, 1, 42, true, false, undefined, [], 118 | function() {}]; 119 | for (var i = 0; i < values.length; ++i) { 120 | var value = values[0]; 121 | expect(this.validator.normalizeProperties(value)).to.equal(null); 122 | expect(this.validator.error).to.be.an.instanceOf(TypeError); 123 | expect(this.validator.error.message).to.equal( 124 | "Invalid properties: " + value); 125 | } 126 | }); 127 | 128 | it("rejects long keys", function() { 129 | properties = {}; 130 | longKey = new Array(1026).join("A"); 131 | properties[longKey] = "value"; 132 | expect(this.validator.normalizeProperties(properties)).to.equal(null); 133 | expect(this.validator.error).to.be.an.instanceOf(RangeError); 134 | expect(this.validator.error.message).to.equal("Property name " + 135 | longKey + " too long;" + 136 | " 1025 is above the 1024-character limit"); 137 | }); 138 | 139 | it("rejects long string values", function() { 140 | longValue = new Array(1026).join("A"); 141 | expect(this.validator.normalizeProperties({ key: longValue })).to.equal( 142 | null); 143 | expect(this.validator.error).to.be.an.instanceOf(RangeError); 144 | expect(this.validator.error.message).to.equal("Property key value " + 145 | longValue + " too long;" + 146 | " 1025 is above the 1024-character limit"); 147 | }); 148 | 149 | it("rejects invalid value types", function() { 150 | var values = [null, true, false, undefined, [], {}, function() {}]; 151 | for (var i = 0; i < values.length; ++i) { 152 | var value = values[0]; 153 | expect(this.validator.normalizeProperties({ key: value })).to.equal( 154 | null); 155 | expect(this.validator.error).to.be.an.instanceOf(TypeError); 156 | expect(this.validator.error.message).to.equal( 157 | "Unsupported type for property key value: " + value); 158 | } 159 | }); 160 | 161 | it("returns the argument", function() { 162 | properties = { key: "value", number: 42 }; 163 | expect(this.validator.normalizeProperties(properties)).to.equal( 164 | properties); 165 | expect(this.validator.error).to.equal(null); 166 | }); 167 | }); 168 | }); 169 | --------------------------------------------------------------------------------