Skip to content

Instantly share code, notes, and snippets.

@ranic
Last active October 16, 2024 19:47
Show Gist options
  • Save ranic/80459104def4e4bcd73d5c77b817ee43 to your computer and use it in GitHub Desktop.
Save ranic/80459104def4e4bcd73d5c77b817ee43 to your computer and use it in GitHub Desktop.
Example of setting up tracking via a proxy.
<html>
<head>
<title>Mixpanel Tracking Proxy Demo</title>
<script type="text/javascript">
/**
* Configuration Variables - CHANGE THESE!
*/
const MIXPANEL_PROJECT_TOKEN = YOUR_MIXPANEL_PROJECT_TOKEN; // e.g. "67e8bfdec29d84ab2d36ae18c57b8535"
const MIXPANEL_PROXY_DOMAIN = YOUR_PROXY_DOMAIN; // e.g. "https://proxy-eoca2pin3q-uc.a.run.app"
/**
* Set the MIXPANEL_CUSTOM_LIB_URL - No need to change this
*/
const MIXPANEL_CUSTOM_LIB_URL = MIXPANEL_PROXY_DOMAIN + "/lib.min.js";
/**
* Load the Mixpanel JS library asyncronously via the js snippet
*/
(function(f,b){if(!b.__SV){var e,g,i,h;window.mixpanel=b;b._i=[];b.init=function(e,f,c){function g(a,d){var b=d.split(".");2==b.length&&(a=a[b[0]],d=b[1]);a[d]=function(){a.push([d].concat(Array.prototype.slice.call(arguments,0)))}}var a=b;"undefined"!==typeof c?a=b[c]=[]:c="mixpanel";a.people=a.people||[];a.toString=function(a){var d="mixpanel";"mixpanel"!==c&&(d+="."+c);a||(d+=" (stub)");return d};a.people.toString=function(){return a.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(" ");
for(h=0;h<i.length;h++)g(a,i[h]);var j="set set_once union unset remove delete".split(" ");a.get_group=function(){function b(c){d[c]=function(){call2_args=arguments;call2=[c].concat(Array.prototype.slice.call(call2_args,0));a.push([e,call2])}}for(var d={},e=["get_group"].concat(Array.prototype.slice.call(arguments,0)),c=0;c<j.length;c++)b(j[c]);return d};b._i.push([e,f,c])};b.__SV=1.2;e=f.createElement("script");e.type="text/javascript";e.async=!0;e.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?
MIXPANEL_CUSTOM_LIB_URL:"file:"===f.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";g=f.getElementsByTagName("script")[0];g.parentNode.insertBefore(e,g)}})(document,window.mixpanel||[]);
/**
* Initialize a Mixpanel instance using your project token and proxy domain
*/
mixpanel.init(MIXPANEL_PROJECT_TOKEN, {debug: true, api_host: MIXPANEL_PROXY_DOMAIN});
/**
* Track an event when the page is loaded
*/
mixpanel.track("[Proxy Demo] Page loaded");
</script>
</head>
<body>
<button onclick="mixpanel.track('[Proxy Demo] Button clicked')">Track event</button>
</body>
</html>
@mgara
Copy link

mgara commented Jun 8, 2023

Hello, thanks for this example, however is there a way to change the endpoint from /track to something else to prevent heuristic algorithms from blocking tracking events ?

@mgara
Copy link

mgara commented Jun 8, 2023

Can I just add the ip address to the payload and the geolocation will be made on your side ? (maxpanel)

@ranic
Copy link
Author

ranic commented Jun 20, 2023

You can't change the endpoint name itself via our SDKs, but you can host your own server that does the proxying.

For example, you could have an endpoint hosted at https://yourdomain.com/e. The handler for that endpoint could simply pass through to Mixpanel's /import API. Then every time you want to fire an event from your client, you make a request to that server.

re: IP, yes if you pass ip explicitly it will be geoencoded on our servers. This is documented here: https://docs.mixpanel.com/docs/tracking/how-tos/effective-server#tracking-geolocation

@StephenHaney
Copy link

Thanks for providing this example. It seems that when I start proxying requests through an nginx instance I'm losing the geolocation ability. I'm definitely passing along the client's IP in the headers (X-Real-IP). Since it's just JS -> nginx -> mixpanel, there's no chance to modify the payload to add an IP property. @ranic can you tell me where mixpanel is expecting to look to pick up the client IP?

@ranic
Copy link
Author

ranic commented Jul 16, 2023

We look at the ip event property. Our SDK sets it automatically if you don't set it yourself, but in this case, it might be easier to set it yourself (include a property called ip on all your events which is set to the client's IP address). You can do this in your client-side tracking code.

See the code example here

@StephenHaney
Copy link

StephenHaney commented Jul 17, 2023

Hmm, I checked the payload when going straight client -> MixPanel and it didn't include an ip property, but MixPanel still calculated geolocation. This makes sense since the front-end code doesn't have a way to know the IP address without doing a server call.

Now I'm doing client -> nginx proxy -> MixPanel. I don't have a way to set ip in the payload (well, I'd have to first call some service to get the IP on the client). In nginx I have the IP, but I don't believe I can easily modify the json payload with an nginx rule.

It's interesting that it worked client -> MixPanel directly, even without an ip property set. I wonder what it uses to determine IP in that setup – and if I can replicate it with the proxy.

@mgara
Copy link

mgara commented Jul 17, 2023

Hello, I did add the ip field and worked perfectly with the server sdk.
Well I had to rebuild the payload and send it in the batch import on the server side using the mixpanel server sdk
Browser -> Server->Append the ip address of the client -> Mixpanel (Node sdk)

@mgara
Copy link

mgara commented Jul 17, 2023

@StephenHaney
This is how i extracted the remote client behind the proxy
const forwarded = req.headers['x-forwarded-for'] const ip = forwarded ? forwarded.split(/\s*,\s*/)[0] : req.socket.remoteAddress;
as the proxies start to pile up the ip addresses of the remote addresses, you will end up having a http header (X-Forwarded-For) with a string that consists of all the ip addresses that the request went through, your first ip address is the client's one.

@StephenHaney
Copy link

StephenHaney commented Jul 17, 2023

Thanks for sharing @mgara! This makes sense if you're using a server. I might end up using this approach too.

In the pure client -> nginx proxy -> MixPanel approach, there is no chance to add an ip field. And if you use a server in the middle, there's not much point in using the nginx proxy at all. The server can just call directly to MixPanel's API.

On the header approach: just to test, I used $remote_addr (which is only the client IP) for both X-Real-IP and X-Forwarded-For headers. I looked at the nginx logs to make sure it sent the correct client IP for both of those headers. But MixPanel still didn't calculate geolocation for those requests.

@StephenHaney
Copy link

For future googlers, something before my proxy was changing $remote_addr to be one hop away client's actual IPs. Often only a few digits different, so hard to notice. For some reason, geolocation wasn't working with these hop IPs.

Instead of using $remote_addr, I switched to using the first IP from $http_x_forwarded_for as my X-Real-IP header that I send to MixPanel. After this, geolocation started working.

Here's the nginx snippet to grab the first value from $http_x_forwarded_for

map $http_x_forwarded_for $original_client_ip {
    # captures everything upto the first comma
    "~^([^,]+)" $1;
    default "";
}

and then later:

proxy_set_header X-Real-IP $original_client_ip;

@fariazz
Copy link

fariazz commented Aug 16, 2023

If we use track users from a proxy (e.g. "exampleproxy.com), are we able to track users across two separate domains (e.g. "example1.com" , "example2.com") if we are using the same proxy server on both domains?

@jalves
Copy link

jalves commented Jan 18, 2024

I'm getting 404 for https://MY_PROXY_DOMAIN/lib.min.js, which also happens if i try directly with https://api.mixpanel.js/lib.min.js.
Am i missing some step?

@ianlet
Copy link

ianlet commented Jan 21, 2024

Hello, thanks for this example, however is there a way to change the endpoint from /track to something else to prevent heuristic algorithms from blocking tracking events ?

@mgara as of 2.48.0, you can configure the endpoint /track to something different. More info here: https://github.com/mixpanel/mixpanel-js/releases/tag/v2.48.0

@horcruxxxx
Copy link

horcruxxxx commented Jan 23, 2024

hello , I used the exact given nginx.conf file, I only changed to a specific port because I am using localhost for running my app also on some other port.
but still got this error.

mixpanel.service.ts:14 Mixpanel error: Bad HTTP status: 0 
mixpanel.service.ts:14 
        POST https://localhost:4999/lib.min.js/track/?verbose=1&ip=1&_=1706010860416 net::ERR_SSL_PROTOCOL_ERROR

here is my nginx.conf file code:

events {}
http {
    server {
        listen 4999 default backlog=16384;
        listen [::]:4999 default backlog=16384;

        location /lib.min.js {
            proxy_set_header X-Real-IP $http_x_forwarded_for;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_pass https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js;
        }

        location /lib.js {
            proxy_set_header X-Real-IP $http_x_forwarded_for;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_pass https://cdn.mxpnl.com/libs/mixpanel-2-latest.js;
        }

        location /decide {
            proxy_set_header Host decide.mixpanel.com;
            proxy_set_header X-Real-IP $http_x_forwarded_for;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_pass https://decide.mixpanel.com/decide;
        }

        location / {
            proxy_set_header Host api.mixpanel.com;
            proxy_set_header X-Real-IP $http_x_forwarded_for;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_pass https://api.mixpanel.com/;
        }
    }
}

and thats my Angular project code

myid = "ae02abe7863092fe2xxxxxxxxxxxx"
 MIXPANEL_CUSTOM_LIB_URL = "https://localhost:4999/lib.min.js";
 constructor() {}
 init() {
   mixpanel.init(this.myid, {
     api_host: this.MIXPANEL_CUSTOM_LIB_URL,
     debug: true,
     track_pageview: true,
   });
 }

@ianlet @mgara @jalves @fariazz @StephenHaney @ranic

@arthabus
Copy link

arthabus commented Oct 7, 2024

Does anyone have a working example of express/nodejs proxy setup?

No matter what I try, geocoding doesn't go through even though X-Real-IP is set (tried first value before comma of x_forwarded_for as suggested by @StephenHaney above and tried other variations too) plus other headers as per example are set. The proxy logs show all values are set and sent to mixpanel but still no luck

@ianlet
Copy link

ianlet commented Oct 7, 2024

@arthabus Does anyone have a working example of express/nodejs proxy setup?

No matter what I try, geocoding doesn't go through even though X-Real-IP is set (tried first value before comma of x_forwarded_for as suggested by @StephenHaney above and tried other variations too) plus other headers as per example are set. The proxy logs show all values are set and sent to mixpanel but still no luck

The proxy we set up is in Deno, but you can probably work your way around it with express/nodejs as it's fairly similar:

import { serve } from "https://deno.land/std@0.192.0/http/server.ts";

const port = Number(Deno.env.get("PORT") ?? 3009);

serve(
  async (request: Request, conn: any) => {
    const { pathname } = new URL(request.url);
    if (pathname.startsWith("/instrumentation")) {
      return await proxyMixpanelCall(request, conn);
    }

    // ... 
  },
  { port },
);

async function proxyMixpanelCall(request: Request, conn: any) {
  const headers = new Headers(request.headers);

  const url = new URL(request.url.replace("/instrumentation", "/track"));
  url.port = "";
  url.protocol = "https:";
  url.host = "api.mixpanel.com";

  const remoteAddress = getRemoteAddress(request, conn);
  const hostname = getHostname(request, url);
  headers.set("Host", "api.mixpanel.com");
  headers.set("X-Real-IP", remoteAddress);
  headers.set("X-Forwarded-For", remoteAddress);
  headers.set("X-Forwarded-Host", hostname);

  const proxyRequest = new Request(url.toString(), {
    method: request.method,
    headers,
    body: request.body,
  });

  try {
    return await fetch(proxyRequest);
  } catch (e) {
    console.log("Failed to proxy call to Mixpanel", e);
    return null;
  }
}

function getRemoteAddress(request: Request, conn: any) {
  return request.headers.get("x-forwarded-for") || conn.remoteAddr?.hostname;
}

function getHostname(request: Request, url: URL) {
  return request.headers.get("x-forwarded-host") || url.hostname;
}

@arthabus
Copy link

@ianlet thanks a lot mate! While I was adopting your solution I realised that I didn't proxy the query params so that was the missing bit. After setting the full mixpanel urls properly with query params it all started to work. Maybe this would help someone in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment