Skip to content

Commit

Permalink
Add Support for Formatting and prioritizing Facebook Click ID
Browse files Browse the repository at this point in the history
  • Loading branch information
alexs-mparticle committed Oct 4, 2024
1 parent e10fdb8 commit 4f66904
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 24 deletions.
80 changes: 67 additions & 13 deletions src/integrationCapture.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import { SDKEventCustomFlags } from './sdkRuntimeModels';
import { Dictionary, queryStringParser, getCookies, getHref } from './utils';

const integrationIdMapping: Dictionary<string> = {
fbclid: 'Facebook.ClickId',
_fbp: 'Facebook.BrowserId',

export const facebookClickIdProcessor = (clickId: string): string => {
if (!clickId) {
return '';
}

const clickIdParts = clickId.split('.');
if (clickIdParts.length === 4) {
return clickIdParts[3];
};

return clickId;
};
interface ProcessorFunction {
(clickId: string): string;
}
interface IntegrationIdMapping {
[key: string]: {
mappingValue: string;
processor?: ProcessorFunction;
};
}

const integrationMapping: IntegrationIdMapping = {
fbclid: {
mappingValue: 'Facebook.ClickId',
processor: facebookClickIdProcessor,
},
_fbp: {
mappingValue: 'Facebook.BrowserId',
},
_fbc: {
mappingValue: 'Facebook.ClickId',
processor: facebookClickIdProcessor,
},
};

export default class IntegrationCapture {
Expand All @@ -13,32 +45,39 @@ export default class IntegrationCapture {
* Captures Integration Ids from cookies and query params and stores them in clickIds object
*/
public capture(): void {
this.captureCookies();
this.captureQueryParams();
const queryParams = this.captureQueryParams() || {};
const cookies = this.captureCookies() || {};

if (queryParams['fbclid'] && cookies['_fbc']) {
// Exclude fbclid if _fbc is present
delete cookies['_fbc'];
}

this.clickIds = { ...this.clickIds, ...queryParams, ...cookies };
}

/**
* Captures cookies based on the integration ID mapping.
*/
public captureCookies(): void {
const cookies = getCookies(Object.keys(integrationIdMapping));
this.clickIds = { ...this.clickIds, ...cookies };
public captureCookies(): Dictionary<string> {
const cookies = getCookies(Object.keys(integrationMapping));
return this.applyProcessors(cookies);
}

/**
* Captures query parameters based on the integration ID mapping.
*/
public captureQueryParams(): void {
const parsedQueryParams = this.getQueryParams();
this.clickIds = { ...this.clickIds, ...parsedQueryParams };
public captureQueryParams(): Dictionary<string> {
const queryParams = this.getQueryParams();
return this.applyProcessors(queryParams);
}

/**
* Gets the query parameters based on the integration ID mapping.
* @returns {Dictionary<string>} The query parameters.
*/
public getQueryParams(): Dictionary<string> {
return queryStringParser(getHref(), Object.keys(integrationIdMapping));
return queryStringParser(getHref(), Object.keys(integrationMapping));
}

/**
Expand All @@ -53,11 +92,26 @@ export default class IntegrationCapture {
}

for (const [key, value] of Object.entries(this.clickIds)) {
const mappedKey = integrationIdMapping[key];
const mappedKey = integrationMapping[key]?.mappingValue;
if (mappedKey) {
customFlags[mappedKey] = value;
}
}
return customFlags;
}

private applyProcessors(clickIds: Dictionary<string>): Dictionary<string> {
const processedClickIds: Dictionary<string> = {};

for (const [key, value] of Object.entries(clickIds)) {
const processor = integrationMapping[key]?.processor;
if (processor) {
processedClickIds[key] = processor(value);
} else {
processedClickIds[key] = value;
}
}

return processedClickIds;
};
}
132 changes: 121 additions & 11 deletions test/jest/integration-capture.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import IntegrationCapture from "../../src/integrationCapture";
import IntegrationCapture, { facebookClickIdProcessor } from "../../src/integrationCapture";
import { deleteAllCookies } from "./utils";

describe('Integration Capture', () => {
Expand All @@ -10,6 +10,25 @@ describe('Integration Capture', () => {
});

describe('#capture', () => {
const originalLocation = window.location;

beforeEach(() => {
delete (window as any).location;
(window as any).location = {
href: '',
search: '',
assign: jest.fn(),
replace: jest.fn(),
reload: jest.fn(),
};

deleteAllCookies();
});

afterEach(() => {
window.location = originalLocation;
});

it('should call captureCookies and captureQueryParams', () => {
const integrationCapture = new IntegrationCapture();
integrationCapture.captureCookies = jest.fn();
Expand All @@ -20,6 +39,67 @@ describe('Integration Capture', () => {
expect(integrationCapture.captureCookies).toHaveBeenCalled();
expect(integrationCapture.captureQueryParams).toHaveBeenCalled();
});

it('should prioritize fbclid over _fbc', () => {

const url = new URL("https://www.example.com/?fbclid=12345&");

window.document.cookie = '_cookie1=1234';
window.document.cookie = '_cookie2=39895811.9165333198';
window.document.cookie = '_fbc=654321';
window.document.cookie = 'baz=qux';

window.location.href = url.href;
window.location.search = url.search;

const integrationCapture = new IntegrationCapture();
integrationCapture.capture();

expect(integrationCapture.clickIds).toEqual({
fbclid: '12345',
});
});

it('should format fbclid correctly', () => {
const url = new URL("https://www.example.com/?fbclid=fb.1.1554763741205.AbCdEfGhIjKlMnOpQrStUvWxYz1234567890");

window.document.cookie = '_cookie1=1234';
window.document.cookie = '_cookie2=39895811.9165333198';
window.document.cookie = '_fbp=54321';
window.document.cookie = 'baz=qux';

window.location.href = url.href;
window.location.search = url.search;

const integrationCapture = new IntegrationCapture();
integrationCapture.capture();

expect(integrationCapture.clickIds).toEqual({
fbclid: 'AbCdEfGhIjKlMnOpQrStUvWxYz1234567890',
'_fbp': '54321',
});
});

it('should format _fbc correctly', () => {
const url = new URL("https://www.example.com/?foo=bar");

window.document.cookie = '_cookie1=1234';
window.document.cookie = '_cookie2=39895811.9165333198';
window.document.cookie = '_fbp=54321';
window.document.cookie = '_fbc=fb.1.1554763741205.AbCdEfGhIjKlMnOpQrStUvWxYz1234567890';
window.document.cookie = 'baz=qux';

window.location.href = url.href;
window.location.search = url.search;

const integrationCapture = new IntegrationCapture();
integrationCapture.capture();

expect(integrationCapture.clickIds).toEqual({
'_fbc': 'AbCdEfGhIjKlMnOpQrStUvWxYz1234567890',
'_fbp': '54321',
});
});
});

describe('#captureQueryParams', () => {
Expand All @@ -41,17 +121,16 @@ describe('Integration Capture', () => {
});

it('should capture specific query params into clickIds object', () => {
const url = new URL("https://www.example.com/?ttclid=12345&fbclid=67890&_fbp=54321");
const url = new URL("https://www.example.com/?ttclid=12345&fbclid=67890&gclid=54321");

window.location.href = url.href;
window.location.search = url.search;

const integrationCapture = new IntegrationCapture();
integrationCapture.captureQueryParams();
const clickIds = integrationCapture.captureQueryParams();

expect(integrationCapture.clickIds).toEqual({
expect(clickIds).toEqual({
fbclid: '67890',
'_fbp': '54321',
});
});

Expand All @@ -62,9 +141,9 @@ describe('Integration Capture', () => {
window.location.search = url.search;

const integrationCapture = new IntegrationCapture();
integrationCapture.captureQueryParams();
const clickIds = integrationCapture.captureQueryParams();

expect(integrationCapture.clickIds).toEqual({});
expect(clickIds).toEqual({});
});
});

Expand All @@ -80,9 +159,9 @@ describe('Integration Capture', () => {
window.document.cookie = 'baz=qux';

const integrationCapture = new IntegrationCapture();
integrationCapture.captureCookies();
const clickIds = integrationCapture.captureCookies();

expect(integrationCapture.clickIds).toEqual({
expect(clickIds).toEqual({
'_fbp': '54321',
});
});
Expand All @@ -93,9 +172,9 @@ describe('Integration Capture', () => {
window.document.cookie = 'baz=qux';

const integrationCapture = new IntegrationCapture();
integrationCapture.captureCookies();
const clickIds = integrationCapture.captureCookies();

expect(integrationCapture.clickIds).toEqual({});
expect(clickIds).toEqual({});
});
});

Expand All @@ -122,4 +201,35 @@ describe('Integration Capture', () => {
expect(customFlags).toEqual({});
});
});

describe('#facebookClickIdProcessor', () => {
it('returns a formatted clickId if it is passed in as a full click id', () => {
const fullClickId = 'fb.1.1554763741205.AbCdEfGhIjKlMnOpQrStUvWxYz1234567890';

const expectedClickId = 'AbCdEfGhIjKlMnOpQrStUvWxYz1234567890';

expect(facebookClickIdProcessor(fullClickId)).toEqual(expectedClickId);
});

it('returns a formatted clickId if it is passed in as a partial click id', () => {
const partialClickId = 'AbCdEfGhIjKlMnOpQrStUvWxYz1234567890';

const expectedClickId = 'AbCdEfGhIjKlMnOpQrStUvWxYz1234567890';

expect(facebookClickIdProcessor(partialClickId)).toEqual(expectedClickId);
});

it('returns an empty string if the clickId is not valid', () => {
const expectedClickId = '';

expect(facebookClickIdProcessor(null)).toEqual(expectedClickId);
expect(facebookClickIdProcessor(undefined)).toEqual(expectedClickId);
expect(facebookClickIdProcessor('')).toEqual(expectedClickId);

// @ts-ignore
expect(facebookClickIdProcessor(NaN)).toEqual(expectedClickId);
// @ts-ignore
expect(facebookClickIdProcessor(0)).toEqual(expectedClickId);
});
});
});
1 change: 1 addition & 0 deletions test/src/tests-feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe('feature-flags', function() {
deleteAllCookies();
sinon.restore(); // Restore all stubs and spies

deleteAllCookies();
});

it('should capture click ids when feature flag is true', () => {
Expand Down

0 comments on commit 4f66904

Please sign in to comment.