Skip to content
This repository was archived by the owner on Apr 27, 2024. It is now read-only.

Commit

Permalink
Merge pull request #35 from john-doherty/add-utm-tracking
Browse files Browse the repository at this point in the history
Add `utm`, `language`, `advert` data
  • Loading branch information
john-doherty authored Mar 6, 2024
2 parents 95ee099 + e399126 commit 96ef2ef
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 118 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14.16.0
node-version: '14.16.0'
- run: npm install
- run: npm test
4 changes: 2 additions & 2 deletions dist/mixpanel-lite.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mixpanel-lite",
"version": "1.5.6",
"description": "A lightweight alternative to mixpanel-js with offline support for Hybrid and Progressive Web Apps",
"version": "1.5.8",
"description": "A lightweight alternative to mixpanel-js with offline support for PWAs",
"main": "src/mixpanel-lite.js",
"scripts": {
"start": "node server/dev-server.js",
Expand All @@ -19,15 +19,14 @@
"keywords": [
"mixpanel",
"offline",
"pwa",
"progressive"
"pwa"
],
"author": "John Doherty <contact@johndoherty.info> (www.johndoherty.info)",
"license": "MIT",
"engineStrict": true,
"engines": {
"node": ">=14.16.0",
"npm": ">=6.14.11"
"node": "14.16.0",
"npm": "6.14.11"
},
"devDependencies": {
"del": "2.2.2",
Expand Down
109 changes: 97 additions & 12 deletions src/mixpanel-lite.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,15 +299,15 @@
// mark sending complete
_sending = false;
})
.catch(function(err) {
.catch(function (err) {

if (_debugging) {
console.log(err);
}
if (_debugging) {
console.log(err);
}

// something went wrong, allow this method to be recalled
_sending = false;
});
// something went wrong, allow this method to be recalled
_sending = false;
});
}

/* #region Helpers */
Expand Down Expand Up @@ -380,12 +380,12 @@
remove: function (itemsToRemove) {

// get array of ids to remove
var idsToRemove = (itemsToRemove || []).map(function(item) {
var idsToRemove = (itemsToRemove || []).map(function (item) {
return item._id;
});

// go through existing transactions, removing items that contain a matching id
var remaining = transactions.all().filter(function(item) {
var remaining = transactions.all().filter(function (item) {
return idsToRemove.indexOf(item._id) === -1;
});

Expand Down Expand Up @@ -513,6 +513,80 @@
return parseFloat(matches[matches.length - 2]);
}

/**
* Get advertising click IDs from the URL.
*
* @returns {Object} An object containing the advertising click IDs found in the URL. The object can have the following properties:
* - facebookClickId {string}: for tracking interactions with Facebook ads
* - doubleClickId {string}: for tracking ads served by Google's DoubleClick
* - googleClickId {string}: for tracking Google Ads campaigns
* - genericClickId {string}: for tracking clicks on certain advertising platforms
* - linkedInClickId {string}: for tracking interactions with LinkedIn ads
* - microsoftClickId {string}: for tracking interactions with Microsoft Advertising
* - tikTokClickId {string}: for tracking interactions with TikTok ads
* - twitterClickId {string}: for tracking interactions with Twitter ads
* - webBrowserReferrerId {string}: for tracking sources of traffic or conversions.
* Each property is included only if its corresponding param exists
*/
function getAdvertisingClickIDs() {

var urlParams = new URLSearchParams(window.location.search || '');
var clickIDs = {};

if (urlParams.has('dclid')) clickIDs.doubleClickId = urlParams.get('dclid');
if (urlParams.has('fbclid')) clickIDs.facebookClickId = urlParams.get('fbclid');
if (urlParams.has('gclid')) clickIDs.googleClickId = urlParams.get('gclid');
if (urlParams.has('ko_click_id')) clickIDs.genericClickId = urlParams.get('ko_click_id');
if (urlParams.has('li_fat_id')) clickIDs.linkedInClickId = urlParams.get('li_fat_id');
if (urlParams.has('msclkid')) clickIDs.microsoftClickId = urlParams.get('msclkid');
if (urlParams.has('ttclid')) clickIDs.tikTokClickId = urlParams.get('ttclid');
if (urlParams.has('twclid')) clickIDs.twitterClickId = urlParams.get('twclid');
if (urlParams.has('wbraid')) clickIDs.webBrowserReferrerId = urlParams.get('wbraid');

return Object.keys(clickIDs).length > 0 ? clickIDs : null;
}

/**
* Get UTM parameters from the URL
* @returns {Object} UTM parameters found in the URL, can have the following properties:
* - source {string}: identifying which site sent the traffic
* - medium {string}: identifying the type of link used
* - campaign {string}: identifying a specific product promotion or campaign
* - term {string}: identifying search terms
* - content {string}: identifying what specifically was clicked to bring the user to the site
* Each property is included only if the param exists
*/
function getUtmParams() {
var params = new URLSearchParams(window.location.search || '');
var utmParams = {};

if (params.has('utm_source')) utmParams.source = params.get('utm_source');
if (params.has('utm_medium')) utmParams.medium = params.get('utm_medium');
if (params.has('utm_campaign')) utmParams.campaign = params.get('utm_campaign');
if (params.has('utm_term')) utmParams.term = params.get('utm_term');
if (params.has('utm_content')) utmParams.content = params.get('utm_content');

return Object.keys(utmParams).length > 0 ? utmParams : null;
}

/**
* Get preferred language from the browser
* @return {string} containing language
*/
function getBrowserLanguage() {

if (navigator.languages && navigator.languages.length) {
return navigator.languages[0]; // first language is preferred
}

// Fallbacks for older browsers
return navigator.language ||
navigator.userLanguage ||
navigator.browserLanguage ||
navigator.systemLanguage ||
'en'; // Default to English if none is found
}

/**
* Gets the referring domain
* @returns {string} domain or empty string
Expand Down Expand Up @@ -644,7 +718,7 @@
track: function (eventName, data) {
console.log('mixpanel.track(\'' + eventName + '\',' + JSON.stringify(data || {}) + ')');
},
register: function(data) {
register: function (data) {
console.log('mixpanel.register(' + JSON.stringify(data || {}) + ')');
},
reset: function () {
Expand Down Expand Up @@ -749,10 +823,21 @@
distinct_id: uuid,
$device_id: uuid,
mp_lib: 'mixpanel-lite',
$lib_version: '0.0.0'
$lib_version: '0.0.0',
language: getBrowserLanguage()
};

// only track page URLs
var utmParams = getUtmParams();
if (utmParams) {
_properties.utm = utmParams;
}

var advertParams = getAdvertisingClickIDs();
if (advertParams) {
_properties.advert = advertParams;
}

// only track page URLs (not file etc)
if (String(window.location.protocol).indexOf('http') === 0) {
_properties.$current_url = window.location.href;
}
Expand Down
149 changes: 149 additions & 0 deletions tests/mixpanel-lite-advert-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
var path = require('path');
var puppeteer = require('puppeteer');
var querystring = require('querystring');
var utils = require('./utils');

jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;

var url = 'file://' + path.join(__dirname, 'environment.html');
var page = null;
var browser = null;

describe('mixpanel-lite UTM', function () {

// create a new browser instance before each test
beforeEach(async function () {

// Launch a new browser instance
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});

// get page
page = (await browser.pages())[0];
});

afterEach(async function () {

return page.evaluate(function () {
return localStorage.removeItem('mixpanel-lite');
})
.then(function () {
return utils.sleep(500);
})
.then(function () {
return browser.close();
});
});

it('should send advert data to /track endpoint', async function () {
var now = (new Date()).getTime();
var token = 'test-token-' + now;
var eventName = 'test-event-' + now;

// open page with UTM params
await page.goto(url + '?dclid=randomDclidValue&fbclid=randomFbclidValue&gclid=randomGclidValue&ko_click_id=randomKoClickIdValue&li_fat_id=randomLiFatIdValue&msclkid=randomMsclkidValue&ttclid=randomTtclidValue&twclid=randomTwclidValue&wbraid=randomWbraidValue');

await page.setRequestInterception(true);

const requestPromise = new Promise((resolve, reject) => {
page.once('request', function (request) {
var requestUrl = request.url();
var query = requestUrl.substr(requestUrl.indexOf('?') + 1);
var params = querystring.parse(query);

try {
// Assertions
expect(requestUrl.startsWith('https://api.mixpanel.com/track')).toBe(true);
expect(params).toBeDefined();
expect(params._).toBeDefined();
expect(params.data).toBeDefined();
expect(params.data).not.toEqual('');

var data = JSON.parse(Buffer.from(params.data, 'base64').toString('ascii'));

expect(data.event).toEqual(eventName);
expect(data.properties).toBeDefined();
expect(data.properties.distinct_id).toBeDefined();
expect(data.properties.$browser).toEqual('Chrome');
expect(data.properties.token).toEqual(token);
expect(data.properties.advert.doubleClickId).toEqual('randomDclidValue');
expect(data.properties.advert.facebookClickId).toEqual('randomFbclidValue');
expect(data.properties.advert.genericClickId).toEqual('randomKoClickIdValue');
expect(data.properties.advert.linkedInClickId).toEqual('randomLiFatIdValue');
expect(data.properties.advert.microsoftClickId).toEqual('randomMsclkidValue');
expect(data.properties.advert.tikTokClickId).toEqual('randomTtclidValue');
expect(data.properties.advert.twitterClickId).toEqual('randomTwclidValue');
expect(data.properties.advert.webBrowserReferrerId).toEqual('randomWbraidValue');

resolve(); // Resolve the promise after assertions
}
catch (error) {
reject(error); // Reject the promise if an assertion fails
}
});
});

// Trigger the tracking
await page.evaluate(function (t, e) {
window.mixpanel.init(t);
window.mixpanel.track(e);
}, token, eventName);

// Wait for the requestPromise to resolve
await requestPromise;
});

it('should NOT send advert data to /track endpoint', async function () {
var now = (new Date()).getTime();
var token = 'test-token-' + now;
var eventName = 'test-event-' + now;

// open page without UTM params
await page.goto(url);

await page.setRequestInterception(true);

const requestPromise = new Promise((resolve, reject) => {
page.once('request', function (request) {
var requestUrl = request.url();
var query = requestUrl.substr(requestUrl.indexOf('?') + 1);
var params = querystring.parse(query);

try {
// Assertions
expect(requestUrl.startsWith('https://api.mixpanel.com/track')).toBe(true);
expect(params).toBeDefined();
expect(params._).toBeDefined();
expect(params.data).toBeDefined();
expect(params.data).not.toEqual('');

var data = JSON.parse(Buffer.from(params.data, 'base64').toString('ascii'));

expect(data.event).toEqual(eventName);
expect(data.properties).toBeDefined();
expect(data.properties.distinct_id).toBeDefined();
expect(data.properties.$browser).toEqual('Chrome');
expect(data.properties.token).toEqual(token);

expect(data.properties.advert).toBeUndefined();

resolve(); // Resolve the promise after assertions
}
catch (error) {
reject(error); // Reject the promise if an assertion fails
}
});
});

// Trigger the tracking
await page.evaluate(function (t, e) {
window.mixpanel.init(t);
window.mixpanel.track(e);
}, token, eventName);

// Wait for the requestPromise to resolve
await requestPromise;
});
});
Loading

0 comments on commit 96ef2ef

Please sign in to comment.