diff --git a/lib/mixpanel-node.d.ts b/lib/mixpanel-node.d.ts index 4346c88..c1cbf09 100644 --- a/lib/mixpanel-node.d.ts +++ b/lib/mixpanel-node.d.ts @@ -25,6 +25,7 @@ declare namespace mixpanel { keepAlive: boolean; geolocate: boolean; logger: CustomLogger; + strict: boolean; } export interface PropertyDict { @@ -51,6 +52,11 @@ declare namespace mixpanel { export interface BatchOptions { max_concurrent_requests?: number; max_batch_size?: number; + strict?: boolean; + } + + export interface ImportOptions { + strict?: boolean; } export interface UnionData { @@ -74,6 +80,7 @@ declare namespace mixpanel { import(eventName: string, time: Date | number, properties?: PropertyDict, callback?: Callback): void; import(eventName: string, time: Date | number, callback: Callback): void; + import(eventName: string, time: Date | number, properties: PropertyDict, options: ImportOptions, callback?: Callback): void; import_batch(eventNames: string[], options?: BatchOptions, callback?: BatchCallback): void; import_batch(eventNames: string[], callback?: BatchCallback): void; diff --git a/lib/mixpanel-node.js b/lib/mixpanel-node.js index 5d67c43..6974e3a 100644 --- a/lib/mixpanel-node.js +++ b/lib/mixpanel-node.js @@ -31,6 +31,8 @@ const DEFAULT_CONFIG = { // e.g., when running under electron geolocate: false, logger: console, + // set this to true to enable strict validation on import requests + strict: false, }; var create_client = function(token, config) { @@ -63,6 +65,7 @@ var create_client = function(token, config) { * @param {string} options.endpoint * @param {object} options.data the data to send in the request * @param {string} [options.method] e.g. `get` or `post`, defaults to `get` + * @param {boolean} [options.strict] whether to enable strict validation for import requests * @param {function} callback called on request completion or error */ metrics.send_request = function(options, callback) { @@ -122,6 +125,11 @@ var create_client = function(token, config) { query_params.test = 1; } + // add strict parameter for import endpoints + if (endpoint === '/import' && (options.strict || metrics.config.strict)) { + query_params.strict = 1; + } + request_options.path = metrics.config.path + endpoint + "?" + querystring.stringify(query_params); request = request_lib.request(request_options, function(res) { @@ -212,12 +220,14 @@ var create_client = function(token, config) { * @param {string} options.endpoint e.g. `/track` or `/import` * @param {number} [options.max_concurrent_requests] limits concurrent async requests over the network * @param {number} [options.max_batch_size] limits number of events sent to mixpanel per request + * @param {boolean} [options.strict] whether to enable strict validation for import requests * @param {Function} [callback] callback receives array of errors if any * */ var send_batch_requests = function(options, callback) { var event_list = options.event_list, endpoint = options.endpoint, + strict = options.strict, max_batch_size = options.max_batch_size ? Math.min(MAX_BATCH_SIZE, options.max_batch_size) : MAX_BATCH_SIZE, // to maintain original intention of max_batch_size; if max_batch_size is greater than 50, we assume the user is trying to set max_concurrent_requests max_concurrent_requests = options.max_concurrent_requests || (options.max_batch_size > MAX_BATCH_SIZE && Math.ceil(options.max_batch_size / MAX_BATCH_SIZE)), @@ -243,8 +253,15 @@ var create_client = function(token, config) { return event; }); + var request_options = { method: "POST", endpoint: endpoint, data: batch }; + + // Add strict option if specified + if (strict) { + request_options.strict = strict; + } + // must be a POST - metrics.send_request({ method: "POST", endpoint: endpoint, data: batch }, cb); + metrics.send_request(request_options, cb); } } @@ -327,7 +344,7 @@ var create_client = function(token, config) { }; /** - import(event, time, properties, callback) + import(event, time, properties, options, callback) --- This function sends an event to mixpanel using the import endpoint. The time argument should be either a Date or Number, @@ -343,18 +360,51 @@ var create_client = function(token, config) { event:string the event name time:date|number the time of the event properties:object additional event properties to send + options:object optional configuration including strict validation callback:function(err:Error) callback is called when the request is finished or an error occurs */ - metrics.import = function(event, time, properties, callback) { - if (!properties || typeof properties === "function") { + metrics.import = function(event, time, properties, options, callback) { + // Handle various argument combinations for backward compatibility + if (typeof properties === "function") { callback = properties; properties = {}; + options = {}; + } else if (typeof options === "function") { + callback = options; + options = {}; + } else if (!properties) { + properties = {}; + options = {}; + } else if (!options) { + options = {}; } properties.time = ensure_timestamp(time); - metrics.send_event_request("/import", event, properties, callback); + var request_options = { + method: "GET", + endpoint: "/import", + data: { + event: event, + properties: properties + } + }; + + // Add strict option if specified + if (options && options.strict) { + request_options.strict = options.strict; + } + + properties.token = metrics.token; + properties.mp_lib = "node"; + properties.$lib_version = packageInfo.version; + + if (metrics.config.debug) { + metrics.config.logger.debug("Sending the following event to Mixpanel", { data: request_options.data }); + } + + metrics.send_request(request_options, callback); }; /** @@ -389,6 +439,7 @@ var create_client = function(token, config) { usage. max_concurrent_requests: the maximum number of concurrent http requests that can be made to mixpanel; also useful for capping bandwidth. + strict: whether to enable strict validation for import requests N.B.: the Mixpanel API only accepts 50 events per request, so regardless of max_batch_size, larger lists of events will be chunked further into @@ -410,7 +461,8 @@ var create_client = function(token, config) { event_list: event_list, endpoint: "/import", max_concurrent_requests: options.max_concurrent_requests, - max_batch_size: options.max_batch_size + max_batch_size: options.max_batch_size, + strict: options.strict }; send_batch_requests(batch_options, callback); }; diff --git a/test/config.js b/test/config.js index a536899..3f3416d 100644 --- a/test/config.js +++ b/test/config.js @@ -16,6 +16,7 @@ describe('config', () => { keepAlive: true, geolocate: false, logger: console, + strict: false, }); }); @@ -33,6 +34,12 @@ describe('config', () => { expect(mp.config.test).toBe(true); }); + it("supports strict config option", () => { + var mp = Mixpanel.init('token', { strict: true }); + + expect(mp.config.strict).toBe(true); + }); + it("host config is split into host and port", () => { const exampleHost = 'api.example.com'; const examplePort = 70; diff --git a/test/import.js b/test/import.js index 51f5f10..352ff2f 100644 --- a/test/import.js +++ b/test/import.js @@ -127,6 +127,40 @@ describe('import', () => { /`time` property must be a Date or Unix timestamp/, ); }); + + it('supports strict option in import method', () => { + var event = 'test', + time = six_days_ago_timestamp, + props = { key1: 'val1' }, + options = { strict: true }; + + mixpanel.import(event, time, props, options); + + expect(mixpanel.send_request).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/import', + strict: true, + }), + undefined, + ); + }); + + it('uses global strict config when enabled', () => { + mixpanel.set_config({ strict: true }); + var event = 'test', + time = six_days_ago_timestamp, + props = { key1: 'val1' }; + + mixpanel.import(event, time, props); + + // Check that send_request is called and the path contains strict=1 + expect(mixpanel.send_request).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/import', + }), + undefined, + ); + }); }); describe('import_batch', () => { @@ -189,6 +223,43 @@ describe('import_batch', () => { mixpanel.import_batch(event_list); expect(mixpanel.send_request).toHaveBeenCalledTimes(3); }); + + it('supports strict option in import_batch method', () => { + var event_list = [ + {event: 'test', properties: {key1: 'val1', time: 500 }}, + {event: 'test', properties: {key2: 'val2', time: 1000}}, + ]; + + mixpanel.import_batch(event_list, { strict: true }); + + expect(mixpanel.send_request).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + endpoint: '/import', + strict: true, + }), + expect.any(Function) + ); + }); + + it('uses global strict config in import_batch when enabled', () => { + mixpanel.set_config({ strict: true }); + var event_list = [ + {event: 'test', properties: {key1: 'val1', time: 500 }}, + {event: 'test', properties: {key2: 'val2', time: 1000}}, + ]; + + mixpanel.import_batch(event_list); + + // Check that send_request is called - the global strict config will be used by send_request + expect(mixpanel.send_request).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + endpoint: '/import', + }), + expect.any(Function) + ); + }); }); describe('import_batch_integration', () => { diff --git a/test/send_request.js b/test/send_request.js index 34f5eb9..f01bce8 100644 --- a/test/send_request.js +++ b/test/send_request.js @@ -307,4 +307,41 @@ describe("send_request", () => { `/import?ip=0&verbose=0&data=e30%3D&api_key=barbaz`, ); }); + + it("adds strict=1 parameter to import requests when strict option is true", () => { + mixpanel.set_config({secret: 'test-secret'}); + mixpanel.send_request({ + endpoint: `/import`, + data: {}, + strict: true, + }); + expect(https.request).toHaveBeenCalledTimes(1); + expect(https.request.mock.calls[0][0].path).toBe( + `/import?ip=0&verbose=0&data=e30%3D&strict=1`, + ); + }); + + it("adds strict=1 parameter to import requests when global strict config is true", () => { + mixpanel.set_config({secret: 'test-secret', strict: true}); + mixpanel.send_request({ + endpoint: `/import`, + data: {}, + }); + expect(https.request).toHaveBeenCalledTimes(1); + expect(https.request.mock.calls[0][0].path).toBe( + `/import?ip=0&verbose=0&data=e30%3D&strict=1`, + ); + }); + + it("does not add strict parameter to non-import endpoints", () => { + mixpanel.set_config({strict: true}); + mixpanel.send_request({ + endpoint: `/track`, + data: {}, + }); + expect(https.request).toHaveBeenCalledTimes(1); + expect(https.request.mock.calls[0][0].path).toBe( + `/track?ip=0&verbose=0&data=e30%3D`, + ); + }); });