From 4f66904a47a974538a649224f05c39a37f415d4a Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 4 Oct 2024 16:30:41 -0400 Subject: [PATCH] Add Support for Formatting and prioritizing Facebook Click ID --- src/integrationCapture.ts | 80 +++++++++++++--- test/jest/integration-capture.spec.ts | 132 +++++++++++++++++++++++--- test/src/tests-feature-flags.ts | 1 + 3 files changed, 189 insertions(+), 24 deletions(-) diff --git a/src/integrationCapture.ts b/src/integrationCapture.ts index 8073ae57..beeefedb 100644 --- a/src/integrationCapture.ts +++ b/src/integrationCapture.ts @@ -1,9 +1,41 @@ import { SDKEventCustomFlags } from './sdkRuntimeModels'; import { Dictionary, queryStringParser, getCookies, getHref } from './utils'; -const integrationIdMapping: Dictionary = { - 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 { @@ -13,24 +45,31 @@ 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 { + 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 { + const queryParams = this.getQueryParams(); + return this.applyProcessors(queryParams); } /** @@ -38,7 +77,7 @@ export default class IntegrationCapture { * @returns {Dictionary} The query parameters. */ public getQueryParams(): Dictionary { - return queryStringParser(getHref(), Object.keys(integrationIdMapping)); + return queryStringParser(getHref(), Object.keys(integrationMapping)); } /** @@ -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): Dictionary { + const processedClickIds: Dictionary = {}; + + 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; + }; } diff --git a/test/jest/integration-capture.spec.ts b/test/jest/integration-capture.spec.ts index fcc24f00..ee263322 100644 --- a/test/jest/integration-capture.spec.ts +++ b/test/jest/integration-capture.spec.ts @@ -1,4 +1,4 @@ -import IntegrationCapture from "../../src/integrationCapture"; +import IntegrationCapture, { facebookClickIdProcessor } from "../../src/integrationCapture"; import { deleteAllCookies } from "./utils"; describe('Integration Capture', () => { @@ -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(); @@ -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', () => { @@ -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', }); }); @@ -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({}); }); }); @@ -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', }); }); @@ -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({}); }); }); @@ -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); + }); + }); }); \ No newline at end of file diff --git a/test/src/tests-feature-flags.ts b/test/src/tests-feature-flags.ts index 03294e67..82a1846f 100644 --- a/test/src/tests-feature-flags.ts +++ b/test/src/tests-feature-flags.ts @@ -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', () => {