diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb0fb07..07c785c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [14.x, 16.x, 18.x] steps: - uses: actions/checkout@v2 diff --git a/history.md b/history.md index 02cdf90..14a7541 100644 --- a/history.md +++ b/history.md @@ -1,3 +1,12 @@ +0.16.0 / 2022-06-02 +================== +* support automatic geolocation with `geolocate` option (thanks tmpvar) +* send library version as property with events (thanks ArsalImam) + +0.15.0 / 2022-05-20 +================== +* use keepAlive by default for requests + 0.14.0 / 2021-10-29 ================== * support $latitude and $longitude in profile operations (thanks wneild) diff --git a/lib/mixpanel-node.js b/lib/mixpanel-node.js index adac3f5..e7dc125 100644 --- a/lib/mixpanel-node.js +++ b/lib/mixpanel-node.js @@ -12,6 +12,8 @@ const Buffer = require('buffer').Buffer; const http = require('http'); const https = require('https'); const HttpsProxyAgent = require('https-proxy-agent'); +const url = require('url'); +const packageInfo = require('../package.json') const {async_all, ensure_timestamp} = require('./utils'); const {MixpanelGroups} = require('./groups'); @@ -24,6 +26,10 @@ const DEFAULT_CONFIG = { host: 'api.mixpanel.com', protocol: 'https', path: '', + keepAlive: true, + // set this to true to automatically geolocate based on the client's ip. + // e.g., when running under electron + geolocate: false, }; var create_client = function(token, config) { @@ -31,17 +37,23 @@ var create_client = function(token, config) { throw new Error("The Mixpanel Client needs a Mixpanel token: `init(token)`"); } - // mixpanel constants - const MAX_BATCH_SIZE = 50; - const TRACK_AGE_LIMIT = 60 * 60 * 24 * 5; - const REQUEST_LIBS = {http, https}; - const proxyPath = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; - const proxyAgent = proxyPath ? new HttpsProxyAgent(proxyPath) : null; - const metrics = { token, config: {...DEFAULT_CONFIG}, }; + const {keepAlive} = metrics.config; + + // mixpanel constants + const MAX_BATCH_SIZE = 50; + const REQUEST_LIBS = {http, https}; + const REQUEST_AGENTS = { + http: new http.Agent({keepAlive}), + https: new https.Agent({keepAlive}), + }; + const proxyPath = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; + const proxyAgent = proxyPath ? new HttpsProxyAgent(Object.assign(url.parse(proxyPath), { + keepAlive, + })) : null; /** * sends an async GET or POST request to mixpanel @@ -59,7 +71,7 @@ var create_client = function(token, config) { const endpoint = options.endpoint; const method = (options.method || 'GET').toUpperCase(); let query_params = { - 'ip': 0, + 'ip': metrics.config.geolocate ? 1 : 0, 'verbose': metrics.config.verbose ? 1 : 0 }; const key = metrics.config.key; @@ -103,9 +115,7 @@ var create_client = function(token, config) { throw new Error("The Mixpanel Client needs a Mixpanel API Secret when importing old events: `init(token, { secret: ... })`"); } - if (proxyAgent) { - request_options.agent = proxyAgent; - } + request_options.agent = proxyAgent || REQUEST_AGENTS[metrics.config.protocol]; if (metrics.config.test) { query_params.test = 1; @@ -163,6 +173,7 @@ var create_client = function(token, config) { metrics.send_event_request = function(endpoint, event, properties, callback) { properties.token = metrics.token; properties.mp_lib = "node"; + properties.$lib_version = packageInfo.version; var data = { event: event, @@ -223,7 +234,6 @@ var create_client = function(token, config) { if (batch.length > 0) { batch = batch.map(function (event) { var properties = event.properties; - if (endpoint === '/import' || event.properties.time) { // usually there will be a time property, but not required for `/track` endpoint event.properties.time = ensure_timestamp(event.properties.time); @@ -283,12 +293,9 @@ var create_client = function(token, config) { properties = {}; } - // time is optional for `track` but must be less than 5 days old if set + // time is optional for `track` if (properties.time) { properties.time = ensure_timestamp(properties.time); - if (properties.time < Date.now() / 1000 - TRACK_AGE_LIMIT) { - throw new Error("`track` not allowed for event more than 5 days old; use `mixpanel.import()`"); - } } metrics.send_event_request("/track", event, properties, callback); diff --git a/lib/utils.js b/lib/utils.js index 76c3966..6e026db 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -39,5 +39,5 @@ exports.ensure_timestamp = function(time) { if (!(time instanceof Date || typeof time === "number")) { throw new Error("`time` property must be a Date or Unix timestamp and is only required for `import` endpoint"); } - return time instanceof Date ? Math.floor(time.getTime() / 1000) : time; + return time instanceof Date ? time.getTime() : time; }; diff --git a/package-lock.json b/package-lock.json index 125815b..d0be8bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mixpanel", - "version": "0.14.0", + "version": "0.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mixpanel", - "version": "0.14.0", + "version": "0.17.0", "license": "MIT", "dependencies": { "https-proxy-agent": "5.0.0" @@ -91,15 +91,19 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "dependencies": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ansi-regex": { @@ -445,9 +449,9 @@ ] }, "node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "node_modules/fast-json-stable-stringify": { @@ -3270,12 +3274,12 @@ } }, "ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -3562,9 +3566,9 @@ "dev": true }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-json-stable-stringify": { diff --git a/package.json b/package.json index 5b3c4e8..0d8b542 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "api", "stats" ], - "version": "0.14.0", + "version": "0.17.0", "homepage": "https://github.com/mixpanel/mixpanel-node", "author": "Carl Sverre", "license": "MIT", diff --git a/readme.md b/readme.md index c47e926..61dd0c8 100644 --- a/readme.md +++ b/readme.md @@ -20,9 +20,14 @@ var Mixpanel = require('mixpanel'); // create an instance of the mixpanel client var mixpanel = Mixpanel.init(''); -// initialize mixpanel client configured to communicate over https +// initialize mixpanel client configured to communicate over http instead of https var mixpanel = Mixpanel.init('', { - protocol: 'https' + protocol: 'http', +}); + +// turn off keepAlive (reestablish connection on each request) +var mixpanel = Mixpanel.init('', { + keepAlive: false, }); // track an event with optional properties @@ -249,6 +254,8 @@ Contributions from: - [Cameron Diver](https://github.com/CameronDiver) - [veerabio](https://github.com/veerabio) - [Will Neild](https://github.com/wneild) + - [Elijah Insua](https://github.com/tmpvar) + - [Arsal Imam](https://github.com/ArsalImam) License ------------------- diff --git a/test/config.js b/test/config.js index de64d4b..65d4f7b 100644 --- a/test/config.js +++ b/test/config.js @@ -13,7 +13,9 @@ exports.config = { verbose: false, host: 'api.mixpanel.com', protocol: 'https', - path: '' + path: '', + keepAlive: true, + geolocate: false, }, "default config is incorrect"); test.done(); }, diff --git a/test/import.js b/test/import.js index 23b4e6c..a5ffcbe 100644 --- a/test/import.js +++ b/test/import.js @@ -5,7 +5,7 @@ var proxyquire = require('proxyquire'), Mixpanel = require('../lib/mixpanel-node'); var mock_now_time = new Date(2016, 1, 1).getTime(), - six_days_ago_timestamp = Math.floor(mock_now_time / 1000) - 60 * 60 * 24 * 6; + six_days_ago_timestamp = mock_now_time - (1000 * 60 * 60 * 24 * 6); exports.import = { setUp: function(next) { @@ -50,7 +50,7 @@ exports.import = { "supports a Date instance greater than 5 days old": function(test) { var event = "test", - time = new Date(six_days_ago_timestamp * 1000), + time = new Date(six_days_ago_timestamp), props = { key1: 'val1' }, expected_endpoint = "/import", expected_data = { @@ -82,7 +82,7 @@ exports.import = { properties: { key1: 'val1', token: 'token', - time: Math.floor(mock_now_time / 1000) + time: mock_now_time } }; @@ -98,7 +98,7 @@ exports.import = { "supports a unix timestamp": function(test) { var event = "test", - time = mock_now_time / 1000, + time = mock_now_time, props = { key1: 'val1' }, expected_endpoint = "/import", expected_data = { @@ -122,7 +122,7 @@ exports.import = { "requires the time argument to be a number or Date": function(test) { test.doesNotThrow(this.mixpanel.import.bind(this, 'test', new Date())); - test.doesNotThrow(this.mixpanel.import.bind(this, 'test', Date.now()/1000)); + test.doesNotThrow(this.mixpanel.import.bind(this, 'test', Date.now())); test.throws( this.mixpanel.import.bind(this, 'test', 'not a number or Date'), /`time` property must be a Date or Unix timestamp/, diff --git a/test/send_request.js b/test/send_request.js index 43907c4..6bdb4b5 100644 --- a/test/send_request.js +++ b/test/send_request.js @@ -119,6 +119,16 @@ exports.send_request = { test.done(); }, + "sets ip=1 when geolocate option is on": function(test) { + this.mixpanel.set_config({ geolocate: true }); + + this.mixpanel.send_request({ method: "get", endpoint: "/track", event: "test", data: {} }); + + test.ok(https.request.calledWithMatch({ path: Sinon.match('ip=1') }), "send_request didn't call http.get with correct request data"); + + test.done(); + }, + "handles mixpanel errors": function(test) { test.expect(1); this.mixpanel.send_request({ endpoint: "/track", data: { event: "test" } }, function(e) { @@ -140,6 +150,30 @@ exports.send_request = { this.http_emitter.emit('error', 'error'); }, + "default use keepAlive agent": function(test) { + test.expect(2); + var agent = new https.Agent({ keepAlive: false }); + var httpsStub = { + request: Sinon.stub().returns(this.http_emitter).callsArgWith(1, this.res), + Agent: Sinon.stub().returns(agent), + }; + // force SDK not use `undefined` string to initialize proxy-agent + delete process.env.HTTP_PROXY + delete process.env.HTTPS_PROXY + Mixpanel = proxyquire('../lib/mixpanel-node', { + 'https': httpsStub + }); + var proxyMixpanel = Mixpanel.init('token'); + proxyMixpanel.send_request({ endpoint: '', data: {} }); + + var getConfig = httpsStub.request.firstCall.args[0]; + var agentOpts = httpsStub.Agent.firstCall.args[0]; + test.ok(agentOpts.keepAlive === true, "HTTP Agent wasn't initialized with keepAlive by default"); + test.ok(getConfig.agent === agent, "send_request didn't call https.request with agent"); + + test.done(); + }, + "uses correct hostname": function(test) { var host = 'testhost.fakedomain'; var customHostnameMixpanel = Mixpanel.init('token', { host: host }); @@ -215,8 +249,9 @@ exports.send_request = { test.ok(HttpsProxyAgent.calledOnce, "HttpsProxyAgent was not called when process.env.HTTP_PROXY was set"); - var proxyPath = HttpsProxyAgent.firstCall.args[0]; - test.ok(proxyPath === 'this.aint.real.https', "HttpsProxyAgent was not called with the correct proxy path"); + var agentOpts = HttpsProxyAgent.firstCall.args[0]; + test.ok(agentOpts.pathname === "this.aint.real.https", "HttpsProxyAgent was not called with the correct proxy path"); + test.ok(agentOpts.keepAlive === true, "HttpsProxyAgent was not called with the correct proxy path"); var getConfig = https.request.firstCall.args[0]; test.ok(getConfig.agent !== undefined, "send_request didn't call https.request with agent"); @@ -234,8 +269,8 @@ exports.send_request = { test.ok(HttpsProxyAgent.calledOnce, "HttpsProxyAgent was not called when process.env.HTTPS_PROXY was set"); - var proxyPath = HttpsProxyAgent.firstCall.args[0]; - test.ok(proxyPath === 'this.aint.real.https', "HttpsProxyAgent was not called with the correct proxy path"); + var proxyOpts = HttpsProxyAgent.firstCall.args[0]; + test.ok(proxyOpts.pathname === 'this.aint.real.https', "HttpsProxyAgent was not called with the correct proxy path"); var getConfig = https.request.firstCall.args[0]; test.ok(getConfig.agent !== undefined, "send_request didn't call https.request with agent"); diff --git a/test/track.js b/test/track.js index 38f0e1a..0b88297 100644 --- a/test/track.js +++ b/test/track.js @@ -1,8 +1,9 @@ -var proxyquire = require('proxyquire'), - Sinon = require('sinon'), - https = require('https'), - events = require('events'), - Mixpanel = require('../lib/mixpanel-node'); +var proxyquire = require('proxyquire'), + Sinon = require('sinon'), + https = require('https'), + events = require('events'), + Mixpanel = require('../lib/mixpanel-node'), + packageInfo = require('../package.json'); var mock_now_time = new Date(2016, 1, 1).getTime(); @@ -90,8 +91,9 @@ exports.track = { event: 'test', properties: { token: 'token', - time: time.getTime() / 1000, - mp_lib: 'node' + time: time.getTime(), + mp_lib: 'node', + $lib_version: packageInfo.version } }; @@ -106,7 +108,7 @@ exports.track = { "supports unix timestamp for time": function(test) { var event = 'test', - time = mock_now_time / 1000, + time = mock_now_time, props = { time: time }, expected_endpoint = "/track", expected_data = { @@ -114,7 +116,8 @@ exports.track = { properties: { token: 'token', time: time, - mp_lib: 'node' + mp_lib: 'node', + $lib_version: packageInfo.version } }; @@ -127,19 +130,6 @@ exports.track = { test.done(); }, - "throws error if time property is older than 5 days": function(test) { - var event = 'test', - time = (mock_now_time - 1000 * 60 * 60 * 24 * 6) / 1000, - props = { time: time }; - - test.throws( - this.mixpanel.track.bind(this, event, props), - /`track` not allowed for event more than 5 days old/, - "track didn't throw an error when time was more than 5 days ago" - ); - test.done(); - }, - "throws error if time is not a number or Date": function(test) { var event = 'test', props = { time: 'not a number or Date' };