mixpanel/mixpanel-android

People.set throwing UnsupportedOperationException

badoualy opened this issue · 6 comments

An exception is thrown when calling set on people
SDK version: 6.5.2

Non-fatal Exception: java.lang.UnsupportedOperationException
       at java.util.Collections$UnmodifiableMap.put(Collections.java:1504)
       at org.json.JSONObject.put(JSONObject.java:1056)
       at com.mixpanel.android.mpmetrics.MixpanelAPI$PeopleImpl.set(MixpanelAPI.java:1888)
       at com.mixpanel.android.mpmetrics.MixpanelAPI$PeopleImpl.setMap(MixpanelAPI.java:1875)

In the mean time I tried to bypass by using unset on all properties, then setOnceMap

@badoualy

I also faces same error when setting data on people like below

mixPanel.people.set(JSONObject().apply {
            put("name".addDollar(),user.vName)
            put("email".addDollar(), user.vEmailId)
            put(KEY_MOBILE_NUMBER, user.vMobileNumber)
            put(KEY_INSTALLED_FROM, user.vReferalPlatform)
        })

I get error as below

Caused by: java.lang.UnsupportedOperationException
        at java.util.Collections$UnmodifiableMap.put(Collections.java:1504)

It is crashing only in release build in debug build working fine.

I have already used pro guard for the Mix Panel SDK.

Please let me know if you find any solution for the same.

Thank you

UPDATE

I downgraded SDK version to 6.5.1 and it's working fine but events are not being logged in mixpanel dashboard

Indeed I forgot to mention it seems to be working fine on debug builds.

@chichi289 FYI I used the following bypass, that seems to work:

    override suspend fun setPeople(properties: Map<String, Any?>) {
        with(mixpanel.people) {
            properties.keys.forEach { unset(it) }
            setOnceMap(properties.filterValues { it != null })
        }
    }

setOnceMap is not building a JsonObject like set is doing, so the bug won't occur

@badoualy Thanks for the suggestions

I imported mix panel library as a module in project and checked the code which causes exception.

@Override
        public void set(JSONObject properties) {
            if (hasOptedOutTracking()) return;
            try {
                final JSONObject sendProperties = new JSONObject(mDeviceInfo);
                for (final Iterator<?> iter = properties.keys(); iter.hasNext(); ) {
                    final String key = (String) iter.next();
                    sendProperties.put(key, properties.get(key));
                }

                final JSONObject message = stdPeopleMessage("$set", sendProperties);
                recordPeopleMessage(message);
            } catch (final JSONException e) {
                MPLog.e(LOGTAG, "Exception setting people properties", e);
            }
        }
   final JSONObject sendProperties = new JSONObject(mDeviceInfo);

Here mDeviceInfo is passed in JSONObject constructure which causes crash as it is constructed as unmodified map as below

mDeviceInfo = Collections.unmodifiableMap(deviceInfo);

So my guess is somewhere it is being modified.

If we remove mDeviceInfo from JSONObject constructor then code is compiled and worked fine in release app.

@badoualy setOnceMap is working as it is not using mDeviceInfo internally

@zihejia Please look into this and provide solutions for the same

Thank you

Ah indeed, it might be linked to a bad dependency. When checking the code, I had another version of JSONObject that's actually not using the map directly, but adding each entry into its interval map (which wouldn't cause the issue)

    public JSONObject(@NonNull Map copyFrom) {
        this();
        Map<?, ?> contentsTyped = (Map<?, ?>) copyFrom;
        for (Map.Entry<?, ?> entry : contentsTyped.entrySet()) {
            /*
             * Deviate from the original by checking that keys are non-null and
             * of the proper type. (We still defer validating the values).
             */
            String key = (String) entry.getKey();
            if (key == null) {
                throw new NullPointerException("key == null");
            }
            nameValuePairs.put(key, wrap(entry.getValue()));
        }
    }

But I also have this version in my classpath:

    public JSONObject(Map var1) {
        this.map = (Map)(var1 == null ? new HashMap() : var1);
    }

Not sure which one mixpanel is supposed to use. Maybe it's another dependency in our project that's overriding the dependency that mixpanel SDK is using?

Edit: ok so in my case, it looks like the socket-io lib that I'm using is using the following dependency:

    <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>20090211</version>
    </dependency>

which is the version using the map passed to the constructor as is without making a copy.
So using an exclude module on the socket-io should fix the issue.

So the issue isn't actually with mixpanel SDK, but maybe a disclaimer about this could be nice in the setup doc

@badoualy Yes I have also used socket io library so after removing json from socket io it is working fine

implementation ('io.socket:socket.io-client:2.1.0'){
      exclude group: 'org.json', module: 'json'
  }

Thanks for the help @badoualy

Thanks guys for bringing this up and finding a solution. I've added this issue to the FAQ section of our documentation.