Skip to content

Improper errors/exceptions message #38

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

Open
a-wakeel opened this issue Sep 5, 2022 · 2 comments
Open

Improper errors/exceptions message #38

a-wakeel opened this issue Sep 5, 2022 · 2 comments

Comments

@a-wakeel
Copy link

a-wakeel commented Sep 5, 2022

When you don't supply the token or the token value is given null the SDK throws exception with message mix panel Server refused to accept messages, they may be malformed which i think is very confusing. I have found (on the internet) that it also throws the same error due to some other errors as well.

It would be good and helpful to have more proper and meaningful errors for such scenarios.

I think the sendData() method could be modified to return something more diverse and meaningful.

@efimmatytsin
Copy link

Im fully agree!

@nedtwigg
Copy link

Just bumped into this today. This is wild.

First off, if you add ?verbose=1 to the client URLs, the response code changes from 0 to a Json string like {"status":0, "error":"blah"}. IMO that should be the default, but at the very least it should be possible to do from java!

Here's some sample code you can copy-paste that puts this info into the exceptions:

MyMixPanelApi
import com.mixpanel.mixpanelapi.ClientDelivery;
import com.mixpanel.mixpanelapi.MixpanelMessageException;
import com.mixpanel.mixpanelapi.MixpanelServerException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;

public class MyMixpanelAPI {
	private static final int BUFFER_SIZE = 256;
	private static final int CONNECT_TIMEOUT_MILLIS = 2000;
	private static final int READ_TIMEOUT_MILLIS = 10000;
	private static final int BATCH_SIZE = 50;
	protected final String mEventsEndpoint;
	protected final String mPeopleEndpoint;
	protected final String mGroupsEndpoint;

	public MyMixpanelAPI() {
		this("https://api.mixpanel.com/track", "https://api.mixpanel.com/engage", "https://api.mixpanel.com/groups");
	}

	public MyMixpanelAPI(String eventsEndpoint, String peopleEndpoint) {
		this(eventsEndpoint, peopleEndpoint, "https://api.mixpanel.com/groups");
	}

	public MyMixpanelAPI(String eventsEndpoint, String peopleEndpoint, String groupsEndpoint) {
		if (eventsEndpoint == null || peopleEndpoint == null || groupsEndpoint == null) {
			throw new IllegalArgumentException("Endpoints cannot be null");
		}
		this.mEventsEndpoint = eventsEndpoint;
		this.mPeopleEndpoint = peopleEndpoint;
		this.mGroupsEndpoint = groupsEndpoint;
	}

	public void sendMessage(JSONObject message) throws MixpanelMessageException, IOException, InvocationTargetException, IllegalAccessException {
		if (message == null) {
			throw new IOException("Message cannot be null");
		}
		ClientDelivery delivery = new ClientDelivery();
		delivery.addMessage(message);
		this.deliver(delivery);
	}

	public void deliver(ClientDelivery toSend) throws IOException, InvocationTargetException, IllegalAccessException {
		deliver(toSend, false);
	}

	public void deliver(ClientDelivery toSend, boolean useIpAddress) throws IOException, InvocationTargetException, IllegalAccessException {
		if (toSend == null) {
			throw new IllegalArgumentException("ClientDelivery cannot be null");
		}

		String ipParameter = "ip=0&verbose=1";

		try {
			// Process events
			String eventsUrl = this.mEventsEndpoint + "?" + ipParameter;
			Method getEventsMessagesMethod = toSend.getClass().getDeclaredMethod("getEventsMessages");
			getEventsMessagesMethod.setAccessible(true);
			@SuppressWarnings("unchecked")
			List<JSONObject> events = (List<JSONObject>) getEventsMessagesMethod.invoke(toSend);
			sendMessages(events, eventsUrl);

			// Process people
			String peopleUrl = this.mPeopleEndpoint + "?" + ipParameter;
			Method getPeopleMessagesMethod = toSend.getClass().getDeclaredMethod("getPeopleMessages");
			getPeopleMessagesMethod.setAccessible(true);
			@SuppressWarnings("unchecked")
			List<JSONObject> people = (List<JSONObject>) getPeopleMessagesMethod.invoke(toSend);
			sendMessages(people, peopleUrl);

			// Process groups
			String groupsUrl = this.mGroupsEndpoint + "?" + ipParameter;
			Method getGroupMessagesMethod = toSend.getClass().getDeclaredMethod("getGroupMessages");
			getGroupMessagesMethod.setAccessible(true);
			List<JSONObject> groupMessages = (List<JSONObject>) getGroupMessagesMethod.invoke(toSend);
			sendMessages(groupMessages, groupsUrl);
		} catch (IOException | NoSuchMethodException e) {
			throw new IOException("Failed to deliver messages to Mixpanel", e);
		}
	}

	protected String encodeDataString(String dataString) throws UnsupportedEncodingException {
		if (dataString == null) {
			throw new IllegalArgumentException("Data string cannot be null");
		}

		try {
			byte[] utf8data = dataString.getBytes(StandardCharsets.UTF_8);
			String base64data = new String(Base64Coder.encode(utf8data));
			return URLEncoder.encode(base64data, StandardCharsets.UTF_8.name());
		} catch (UnsupportedEncodingException e) {
			throw new UnsupportedEncodingException("Failed to encode data string: " + e.getMessage());
		}
	}

	protected boolean sendData(String dataString, String endpointUrl) throws IOException {
		if (dataString == null || endpointUrl == null) {
			throw new IllegalArgumentException("Data string and endpoint URL cannot be null");
		}

		HttpURLConnection conn = null;
		OutputStream postStream = null;
		InputStream responseStream = null;
		InputStream errorStream = null;

		try {
			URL endpoint = new URL(endpointUrl);
			conn = (HttpURLConnection) endpoint.openConnection();
			conn.setReadTimeout(READ_TIMEOUT_MILLIS);
			conn.setConnectTimeout(CONNECT_TIMEOUT_MILLIS);
			conn.setDoOutput(true);
			conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=utf8");

			String encodedData = encodeDataString(dataString);
			String encodedQuery = "data=" + encodedData;

			// Send data
			postStream = conn.getOutputStream();
			postStream.write(encodedQuery.getBytes(StandardCharsets.UTF_8));
			postStream.flush();

			// Read response
			try {
				responseStream = conn.getInputStream();
				String response = slurp(responseStream);
				if (!response.contains("\"status\":1")) {
					throw new IOException("Server response: " + response + "\n\ndataString: " + dataString);
				}
				return true;
			} catch (IOException e) {
				// Try to read error stream if available
				errorStream = conn.getErrorStream();
				if (errorStream != null) {
					String errorResponse = slurp(errorStream);
					throw new IOException("Failed to send data to Mixpanel. Server response: " + errorResponse, e);
				}
				throw e;
			}
		} catch (SocketTimeoutException e) {
			throw new IOException("Connection timed out while sending data to Mixpanel", e);
		} catch (IOException e) {
			throw new IOException("Failed to send data to Mixpanel", e);
		} finally {
			// Close streams in reverse order of creation
			closeQuietly(errorStream);
			closeQuietly(responseStream);
			closeQuietly(postStream);
			if (conn != null) {
				conn.disconnect();
			}
		}
	}

	private void sendMessages(List<JSONObject> messages, String endpointUrl) throws IOException {
		if (messages == null || endpointUrl == null) {
			throw new IllegalArgumentException("Messages list and endpoint URL cannot be null");
		}

		for (int i = 0; i < messages.size(); i += BATCH_SIZE) {
			int endIndex = Math.min(i + BATCH_SIZE, messages.size());
			List<JSONObject> batch = messages.subList(i, endIndex);

			if (!batch.isEmpty()) {
				try {
					String messagesString = dataString(batch);
					boolean accepted = sendData(messagesString, endpointUrl);

					if (!accepted) {
						throw new MixpanelServerException("Server refused to accept messages, they may be malformed.", batch);
					}
				} catch (IOException e) {
					throw new IOException("Failed to send batch " + (i / BATCH_SIZE + 1), e);
				}
			}
		}
	}

	private String dataString(List<JSONObject> messages) {
		if (messages == null) {
			throw new IllegalArgumentException("Messages list cannot be null");
		}

		JSONArray array = new JSONArray();
		for (JSONObject message : messages) {
			if (message != null) {
				array.put(message);
			}
		}
		return array.toString();
	}

	private String slurp(InputStream in) throws IOException {
		if (in == null) {
			throw new IllegalArgumentException("InputStream cannot be null");
		}

		StringBuilder out = new StringBuilder();
		try (InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
			char[] readBuffer = new char[BUFFER_SIZE];
			int readCount;

			while ((readCount = reader.read(readBuffer)) != -1) {
				out.append(readBuffer, 0, readCount);
			}
		}
		return out.toString();
	}

	private void closeQuietly(AutoCloseable closeable) {
		if (closeable != null) {
			try {
				closeable.close();
			} catch (Exception e) {
				System.err.println("Failed to close resource: " + e.getMessage());
			}
		}
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants