Skip to content

Add support for strict=1 parameter in import API for enhanced validation #238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/mixpanel-node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ declare namespace mixpanel {
keepAlive: boolean;
geolocate: boolean;
logger: CustomLogger;
strict: boolean;
}

export interface PropertyDict {
Expand All @@ -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 {
Expand All @@ -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;
Expand Down
64 changes: 58 additions & 6 deletions lib/mixpanel-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)),
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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);
};

/**
Expand Down Expand Up @@ -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
Expand All @@ -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);
};
Expand Down
7 changes: 7 additions & 0 deletions test/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('config', () => {
keepAlive: true,
geolocate: false,
logger: console,
strict: false,
});
});

Expand All @@ -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;
Expand Down
71 changes: 71 additions & 0 deletions test/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
37 changes: 37 additions & 0 deletions test/send_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
});
});