From 02138be9a32d267bce538c14f8f77c99357f112b Mon Sep 17 00:00:00 2001
From: Jaissica
Date: Mon, 3 Nov 2025 10:30:16 -0500
Subject: [PATCH 01/11] feat(roktManager): add local SHA-256 hashing logic in
hashAttributes method
---
src/roktManager.ts | 41 ++++++++++++++++++++++++++++++++++-------
1 file changed, 34 insertions(+), 7 deletions(-)
diff --git a/src/roktManager.ts b/src/roktManager.ts
index bdb8089e4..17ba9aaf2 100644
--- a/src/roktManager.ts
+++ b/src/roktManager.ts
@@ -227,15 +227,25 @@ export default class RoktManager {
}
}
- public hashAttributes(attributes: IRoktPartnerAttributes): Promise {
- if (!this.isReady()) {
- return this.deferredCall('hashAttributes', attributes);
- }
-
+ public async hashAttributes(attributes: IRoktPartnerAttributes): Promise> {
+ const hashedAttributes: Record = {};
try {
- return this.kit.hashAttributes(attributes);
+ const attributeEntries = Object.entries(attributes ?? {});
+ const hashingTasks = attributeEntries
+ .filter(([, value]) => value !== undefined && value !== null)
+ .map(async ([key, value]) => {
+ const normalizedAttributeValue = String(value).trim().toLocaleLowerCase();
+ const hashedKey = `${key}sha256`;
+ const hashedValue = await this.sha256Hex(normalizedAttributeValue);
+ hashedAttributes[hashedKey] = hashedValue;
+ });
+
+ await Promise.all(hashingTasks);
+ return hashedAttributes;
} catch (error) {
- return Promise.reject(error instanceof Error ? error : new Error('Unknown error occurred'));
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.logger.error('Failed hashAttributes: ' + errorMessage);
+ return Promise.reject(new Error(String(error)));
}
}
@@ -373,4 +383,21 @@ export default class RoktManager {
this.messageQueue.delete(messageId);
}
+
+ private async sha256Hex(input: string): Promise {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(input);
+ const digest = await crypto.subtle.digest('SHA-256', data);
+ return this.arrayBufferToHex(digest);
+ }
+
+ private arrayBufferToHex(buffer: ArrayBuffer): string {
+ const bytes = new Uint8Array(buffer);
+ let hexString = '';
+ for (let i = 0; i < bytes.length; i++) {
+ const hexByte = bytes[i].toString(16).padStart(2, '0');
+ hexString += hexByte;
+ }
+ return hexString;
+ }
}
From ae0da661ab94f0f452b8959b492e412102bd4e58 Mon Sep 17 00:00:00 2001
From: Jaissica
Date: Mon, 3 Nov 2025 15:18:40 -0500
Subject: [PATCH 02/11] added tests for roktManager.hashAttributes in
roktManager.spec.ts
---
test/jest/roktManager.spec.ts | 194 ++++++++++++++++------------------
1 file changed, 93 insertions(+), 101 deletions(-)
diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts
index 3c3bccae2..7e072dbec 100644
--- a/test/jest/roktManager.spec.ts
+++ b/test/jest/roktManager.spec.ts
@@ -2,7 +2,7 @@ import { IKitConfigs } from "../../src/configAPIClient";
import { IMParticleUser } from "../../src/identity-user-interfaces";
import { SDKIdentityApi } from "../../src/identity.interfaces";
import { IMParticleWebSDKInstance } from "../../src/mp-instance";
-import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions } from "../../src/roktManager";
+import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions, IRoktPartnerAttributes, IRoktLauncher, IRoktSelection } from "../../src/roktManager";
import { testMPID } from '../src/config/constants';
const resolvePromise = () => new Promise(resolve => setTimeout(resolve, 0));
@@ -76,82 +76,68 @@ describe('RoktManager', () => {
});
describe('#hashAttributes', () => {
+ interface Hasher {
+ sha256Hex(input: string): Promise
+ }
+
beforeEach(() => {
roktManager['currentUser'] = currentUser;
});
- it('should call kit.hashAttributes with empty attributes', () => {
- const kit: IRoktKit = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
- filters: undefined,
- filteredUser: undefined,
- hashAttributes: jest.fn(),
- selectPlacements: jest.fn(),
- setExtensionData: jest.fn(),
- use: jest.fn(),
- userAttributes: undefined,
- };
-
- roktManager.attachKit(kit);
+ it('should not hash when calling roktManager.hashAttributes with empty attributes and no kit', async () => {
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ const attributes: IRoktPartnerAttributes = {};
+ const messageQueueSizeBefore = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ const messageQueueSizeAfter = roktManager['messageQueue'].size;
+ expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore);
+ expect(shaSpy).not.toHaveBeenCalled();
+ shaSpy.mockRestore();
+ });
- const attributes = {};
+ it('should hash when calling roktManager.hashAttributes without kit', async () => {
+ const nodeCrypto = require('crypto');
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ shaSpy.mockImplementation((s: any) =>
+ Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')),
+ );
- roktManager.hashAttributes(attributes);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
- });
+ const attributes: IRoktPartnerAttributes = { email: 'Jane.Doe@Gmail.com', phone: ' 1234567890 ' };
+ const messageQueueSizeBefore = roktManager['messageQueue'].size;
- it('should call kit.hashAttributes with passed in attributes', () => {
- const kit: IRoktKit = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
- filters: undefined,
- filteredUser: undefined,
- hashAttributes: jest.fn(),
- selectPlacements: jest.fn(),
- setExtensionData: jest.fn(),
- use: jest.fn(),
- userAttributes: undefined,
- };
+ await roktManager.hashAttributes(attributes);
- roktManager.attachKit(kit);
+ const messageQueueSizeAfter = roktManager['messageQueue'].size;
+ expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore);
- const attributes = {
- email: 'test@example.com',
- phone: '1234567890'
- };
+ expect(shaSpy).toHaveBeenCalledWith('jane.doe@gmail.com');
+ expect(shaSpy).toHaveBeenCalledWith('1234567890');
+ expect(shaSpy).toHaveBeenCalledTimes(2);
- roktManager.hashAttributes(attributes);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
+ shaSpy.mockRestore();
});
- it('should queue the hashAttributes method if no launcher or kit is attached', () => {
- const attributes = {
- email: 'test@example.com'
- };
+ it('should not queue when calling roktManager.hashAttributes with no kit attached', async () => {
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex').mockResolvedValue('deadbeef');
- roktManager.hashAttributes(attributes);
+ const attributes: IRoktPartnerAttributes = { email: 'test@example.com' };
+ const messageQueueSizeBefore = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ const messageQueueSizeAfter = roktManager['messageQueue'].size;
+ expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore);
- expect(roktManager['kit']).toBeNull();
- expect(roktManager['messageQueue'].size).toBe(1);
- const queuedMessage = Array.from(roktManager['messageQueue'].values())[0];
- expect(queuedMessage.methodName).toBe('hashAttributes');
- expect(queuedMessage.payload).toBe(attributes);
+ shaSpy.mockRestore();
});
- it('should process queued hashAttributes calls once the launcher and kit are attached', () => {
+ it('should remain non-deferred before and after attaching kit', async () => {
+ const nodeCrypto = require('crypto');
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ shaSpy.mockImplementation((s: any) =>
+ Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')),
+ );
+
const kit: IRoktKit = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
+ launcher: { selectPlacements: jest.fn(), hashAttributes: jest.fn(), use: jest.fn() },
filters: undefined,
filteredUser: undefined,
hashAttributes: jest.fn(),
@@ -161,52 +147,55 @@ describe('RoktManager', () => {
userAttributes: undefined,
};
- const attributes = {
- email: 'test@example.com'
- };
+ const attributes: IRoktPartnerAttributes = { email: 'test@example.com' };
- roktManager.hashAttributes(attributes);
- expect(roktManager['kit']).toBeNull();
- expect(roktManager['messageQueue'].size).toBe(1);
- const queuedMessage = Array.from(roktManager['messageQueue'].values())[0];
- expect(queuedMessage.methodName).toBe('hashAttributes');
- expect(queuedMessage.payload).toBe(attributes);
- expect(kit.hashAttributes).not.toHaveBeenCalled();
+ const sizeBefore = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ expect(roktManager['messageQueue'].size).toBe(sizeBefore);
roktManager.attachKit(kit);
- expect(roktManager['kit']).not.toBeNull();
- expect(roktManager['messageQueue'].size).toBe(0);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
+ const sizeAfterAttach = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ expect(roktManager['messageQueue'].size).toBe(sizeAfterAttach);
+
+ shaSpy.mockRestore();
});
- it('should pass through the correct attributes to kit.launcher.hashAttributes', async () => {
+ it('should allow calling launcher.hashAttributes directly when kit is attached', async () => {
+ const launcher: IRoktLauncher = {
+ selectPlacements: jest.fn().mockResolvedValue({ close: jest.fn(), getPlacements: jest.fn().mockResolvedValue([]) } as unknown as IRoktSelection),
+ hashAttributes: jest.fn(),
+ use: jest.fn().mockResolvedValue(undefined as unknown as never),
+ };
const kit: Partial = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
-
- // We are mocking the hashAttributes method to return the
- // launcher's hashAttributes method and verify that
- // both the kit's and the launcher's methods
- // are called with the correct attributes.
- // This will happen through the Web Kit's hashAttributes method
- hashAttributes: jest.fn().mockImplementation((attributes) => {
- return kit.launcher.hashAttributes(attributes);
- })
+ launcher,
+ hashAttributes: jest.fn(),
};
-
roktManager.attachKit(kit as IRoktKit);
+ const attributes: IRoktPartnerAttributes = { email: 'test@example.com', phone: '1234567890' };
+ await launcher.hashAttributes(attributes);
+ expect(launcher.hashAttributes).toHaveBeenCalledWith(attributes);
+ });
- const attributes = {
- email: 'test@example.com',
- phone: '1234567890'
+ it('should match node crypto output', async () => {
+ const nodeCrypto = require('crypto');
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ shaSpy.mockImplementation((s: any) =>
+ Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')),
+ );
+ const attributes: IRoktPartnerAttributes = {
+ email: ' Jane.DOE@GMAIL.com ',
+ phone: ' 1234567890 ',
+ blank: ' ',
};
-
- roktManager.hashAttributes(attributes);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
- expect(kit.launcher.hashAttributes).toHaveBeenCalledWith(attributes);
+ const fromManager = await roktManager.hashAttributes(attributes);
+ const expected = {
+ emailsha256: nodeCrypto.createHash('sha256').update('jane.doe@gmail.com').digest('hex'),
+ phonesha256: nodeCrypto.createHash('sha256').update('1234567890').digest('hex'),
+ blanksha256: nodeCrypto.createHash('sha256').update('').digest('hex'),
+ };
+ expect(fromManager).toStrictEqual(expected);
+ shaSpy.mockRestore();
});
});
@@ -426,7 +415,7 @@ describe('RoktManager', () => {
expect(kit.selectPlacements).toHaveBeenCalledTimes(3);
});
- it('should call RoktManager methods (not kit methods directly) when processing queue', () => {
+ it('should call RoktManager methods (not kit methods directly) when processing queue', async () => {
// Queue some calls before kit is ready (these will be deferred)
const selectOptions = { attributes: { test: 'value' } } as IRoktSelectPlacementsOptions;
const hashAttrs = { email: 'test@example.com' };
@@ -434,14 +423,12 @@ describe('RoktManager', () => {
const useName = 'TestExtension';
roktManager.selectPlacements(selectOptions);
- roktManager.hashAttributes(hashAttrs);
roktManager.setExtensionData(extensionData);
roktManager.use(useName);
// Verify calls were queued
- expect(roktManager['messageQueue'].size).toBe(4);
+ expect(roktManager['messageQueue'].size).toBe(3);
expect(kit.selectPlacements).not.toHaveBeenCalled(); // Kit methods not called yet
- expect(kit.hashAttributes).not.toHaveBeenCalled(); // Kit methods not called yet
expect(kit.setExtensionData).not.toHaveBeenCalled(); // Kit methods not called yet
expect(kit.use).not.toHaveBeenCalled(); // Kit methods not called yet
@@ -451,6 +438,8 @@ describe('RoktManager', () => {
const setExtensionDataSpy = jest.spyOn(roktManager, 'setExtensionData');
const useSpy = jest.spyOn(roktManager, 'use');
+ const shaSpy = jest.spyOn(roktManager as any, 'sha256Hex').mockResolvedValue('deadbeef');
+ await roktManager.hashAttributes(hashAttrs);
// Attach kit (triggers processMessageQueue)
roktManager.attachKit(kit);
@@ -460,7 +449,7 @@ describe('RoktManager', () => {
expect(hashAttributesSpy).toHaveBeenCalledTimes(1);
expect(hashAttributesSpy).toHaveBeenCalledWith(hashAttrs);
-
+
expect(setExtensionDataSpy).toHaveBeenCalledTimes(1);
expect(setExtensionDataSpy).toHaveBeenCalledWith(extensionData);
@@ -475,6 +464,7 @@ describe('RoktManager', () => {
hashAttributesSpy.mockRestore();
setExtensionDataSpy.mockRestore();
useSpy.mockRestore();
+ shaSpy.mockRestore();
});
it('should preserve RoktManager preprocessing logic when processing deferred selectPlacements calls', () => {
@@ -629,6 +619,7 @@ describe('RoktManager', () => {
});
it('should process queued selectPlacements calls once the launcher and kit are attached', async () => {
+ const shaSpy = jest.spyOn(roktManager as any, 'sha256Hex').mockResolvedValue('deadbeef');
const expectedResult = { placements: ['placement1', 'placement2'] };
const kit: IRoktKit = {
launcher: {
@@ -675,6 +666,7 @@ describe('RoktManager', () => {
expect(roktManager['messageQueue'].size).toBe(0);
expect(kit.selectPlacements).toHaveBeenCalledWith(options);
expect(result).toEqual(expectedResult);
+ shaSpy.mockRestore();
});
it('should pass through the correct attributes to kit.selectPlacements', () => {
From 8c5749d42bdfe3c4a2ef0ca8e58d17705132218e Mon Sep 17 00:00:00 2001
From: Jaissica
Date: Wed, 5 Nov 2025 09:26:30 -0500
Subject: [PATCH 03/11] feat: add BrowserStack beta suite and workflow (#1074)
---
.../workflows/cross-browser-testing-beta.yml | 40 ++++++
package.json | 2 +
src/persistence.js | 6 +-
.../browserstack.karma.beta.config.js | 127 ++++++++++++++++++
4 files changed, 170 insertions(+), 5 deletions(-)
create mode 100644 .github/workflows/cross-browser-testing-beta.yml
create mode 100644 test/cross-browser-testing/browserstack.karma.beta.config.js
diff --git a/.github/workflows/cross-browser-testing-beta.yml b/.github/workflows/cross-browser-testing-beta.yml
new file mode 100644
index 000000000..c4eb7ecd6
--- /dev/null
+++ b/.github/workflows/cross-browser-testing-beta.yml
@@ -0,0 +1,40 @@
+name: 'BrowserStack Beta Browsers Test'
+on: [push, workflow_dispatch]
+
+jobs:
+ browserstack-beta-test:
+ name: 'BrowserStack Beta Browsers Test'
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'BrowserStack Env Setup'
+ uses: browserstack/github-actions/setup-env@master
+ with:
+ username: ${{ secrets.BROWSERSTACK_USERNAME }}
+ access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
+
+ - name: 'BrowserStack Local Tunnel Setup'
+ uses: browserstack/github-actions/setup-local@master
+ with:
+ local-testing: start
+ local-identifier: random
+
+ - name: 'Checkout the repository'
+ uses: actions/checkout@v3
+
+ - name: 'Run NPM CI'
+ run: npm ci
+
+ - name: Run Build IIFE
+ run: npm run build:iife
+
+ - name: 'Run Build test bundle'
+ run: npm run build:test-bundle
+
+ - name: 'Run BrowserStack Beta Browsers Test'
+ run: npm run test:browserstack-beta
+
+ - name: 'BrowserStackLocal Stop'
+ uses: browserstack/github-actions/setup-local@master
+ with:
+ local-testing: stop
+
diff --git a/package.json b/package.json
index 3aea987fc..e082f135a 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,8 @@
"test": "npm run build && npm run build:test-bundle && cross-env DEBUG=false karma start test/karma.config.js",
"test:browserstack": "karma start test/cross-browser-testing/browserstack.karma.config.js",
"test:browserstack:debug": "cross-env DEBUG=true karma start test/cross-browser-testing/browserstack.karma.config.js",
+ "test:browserstack-beta": "karma start test/cross-browser-testing/browserstack.karma.beta.config.js",
+ "test:browserstack-beta:debug": "cross-env DEBUG=true karma start test/cross-browser-testing/browserstack.karma.beta.config.js",
"test:debug": "cross-env DEBUG=true karma start test/karma.config.js",
"test:stub": "cross-env TESTTYPE=stub ENVIRONMENT=prod rollup --config rollup.test.config.js && karma start test/stub/karma.stub.config.js",
"test:integrations": "npm run test:requirejs && npm run test:integrations:cjs && npm run test:integrations:module",
diff --git a/src/persistence.js b/src/persistence.js
index 256595434..fb0aec710 100644
--- a/src/persistence.js
+++ b/src/persistence.js
@@ -949,11 +949,7 @@ export default function _Persistence(mpInstance) {
};
this.resetPersistence = function() {
- removeLocalStorage(StorageNames.localStorageName);
- removeLocalStorage(StorageNames.localStorageNameV3);
- removeLocalStorage(StorageNames.localStorageNameV4);
- removeLocalStorage(mpInstance._Store.storageName);
- removeLocalStorage(StorageNames.localStorageProductsV4);
+ localStorage.clear();
self.expireCookies(StorageNames.cookieName);
self.expireCookies(StorageNames.cookieNameV2);
diff --git a/test/cross-browser-testing/browserstack.karma.beta.config.js b/test/cross-browser-testing/browserstack.karma.beta.config.js
new file mode 100644
index 000000000..9887d171e
--- /dev/null
+++ b/test/cross-browser-testing/browserstack.karma.beta.config.js
@@ -0,0 +1,127 @@
+const { DEBUG } = process.env;
+
+const files = [
+ '../lib/geomock.js',
+ '../../dist/mparticle.js',
+ '../test-bundle.js',
+];
+
+let captureConsole = false;
+let browserConsoleLogOptions = {};
+
+if (DEBUG === 'true') {
+ browserConsoleLogOptions = {
+ level: 'log',
+ format: '%b %T: %m',
+ terminal: true,
+ };
+ captureConsole = true;
+} else {
+ browserConsoleLogOptions = {
+ terminal: false,
+ };
+}
+
+const customLaunchers = {
+ bs_chrome_mac_tahoe_beta: {
+ base: 'BrowserStack',
+ browser: 'chrome',
+ browser_version: 'latest-beta',
+ os: 'OS X',
+ os_version: 'Tahoe' // macOS 26
+ },
+ bs_chrome_mac_sequoia_beta: {
+ base: 'BrowserStack',
+ browser: 'chrome',
+ browser_version: 'latest-beta',
+ os: 'OS X',
+ os_version: 'Sequoia' // macOS 15
+ },
+ bs_chrome_win_beta: {
+ base: 'BrowserStack',
+ browser: 'chrome',
+ browser_version: 'latest',
+ os: 'Windows',
+ os_version: '11'
+ },
+ bs_firefox_mac_tahoe_beta: {
+ base: 'BrowserStack',
+ browser: 'firefox',
+ browser_version: 'latest-beta',
+ os: 'OS X',
+ os_version: 'Tahoe' // macOS 26
+ },
+ bs_firefox_mac_sequoia_beta: {
+ base: 'BrowserStack',
+ browser: 'firefox',
+ browser_version: 'latest-beta',
+ os: 'OS X',
+ os_version: 'Sequoia' // macOS 15
+ },
+ bs_firefox_mac_catalina_beta: {
+ base: 'BrowserStack',
+ browser: 'firefox',
+ browser_version: 'latest-beta',
+ os: 'OS X',
+ os_version: 'Catalina' // macOS 10.15
+ },
+ bs_firefox_win_latest: {
+ base: 'BrowserStack',
+ browser: 'firefox',
+ browser_version: 'latest',
+ os: 'Windows',
+ os_version: '11',
+ },
+ bs_edge_mac_tahoe_beta: {
+ base: 'BrowserStack',
+ browser: 'edge',
+ browser_version: 'latest-beta',
+ os: 'OS X',
+ os_version: 'Tahoe' // macOS 26
+ },
+ bs_edge_mac_sequoia_beta: {
+ base: 'BrowserStack',
+ browser: 'edge',
+ browser_version: 'latest-beta',
+ os: 'OS X',
+ os_version: 'Sequoia' // macOS 15
+ },
+ bs_edge_win_beta: {
+ base: 'BrowserStack',
+ browser: 'edge',
+ browser_version: 'latest-beta',
+ os: 'Windows',
+ os_version: '11'
+ },
+};
+
+module.exports = function(config) {
+ config.set({
+ browserStack: {
+ username: process.env.BS_USERNAME,
+ accessKey: process.env.BS_ACCESS_KEY
+ },
+ autoWatch: false,
+ customLaunchers,
+ browsers: Object.keys(customLaunchers),
+ frameworks: ['mocha', 'should'],
+ files,
+ reporters: ['progress', 'junit'],
+ colors: true,
+ singleRun: true,
+ debug: true,
+ logLevel: config.LOG_INFO,
+ browserConsoleLogOptions,
+ client: {
+ captureConsole,
+ },
+ junitReporter: {
+ outputDir: 'reports/',
+ outputFile: 'test-karma-beta.xml',
+ },
+ browserDisconnectTimeout: 50000,
+ browserDisconnectTolerance: 5,
+ concurrency: 5,
+ });
+};
+
From f7977e82accbc28ea3f8b007ef9e0e1b402eb2d5 Mon Sep 17 00:00:00 2001
From: mparticle-automation
Date: Mon, 10 Nov 2025 14:55:18 +0000
Subject: [PATCH 04/11] chore(build): Generate latest bundle [skip ci]
---
dist/mparticle.common.js | 6 +++---
dist/mparticle.esm.js | 6 +++---
dist/mparticle.js | 14 ++------------
3 files changed, 8 insertions(+), 18 deletions(-)
diff --git a/dist/mparticle.common.js b/dist/mparticle.common.js
index 65856afe8..2246cab4b 100644
--- a/dist/mparticle.common.js
+++ b/dist/mparticle.common.js
@@ -15,7 +15,7 @@ map:function map(a,b){var c,d,e;if(null===this)throw new TypeError(" this is nul
filter:function filter(a/*, thisArg*/){if(void 0===this||null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;ff);o++)n=h[o],b[n]?(a.Logger.verb
if(d[g].mpid){var h=d[g].ui;for(var i in h)if(e===i&&b.userIdentities[e]===h[i]){c=g;break}}c&&f.storeDataInMemory(d,c);},this.encodePersistence=function(b){for(var c in b=JSON.parse(b),b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]?Array.isArray(b.gs[c])&&b.gs[c].length||a._Helpers.isObject(b.gs[c])&&Object.keys(b.gs[c]).length?b.gs[c]=Base64.encode(JSON.stringify(b.gs[c])):delete b.gs[c]:delete b.gs[c]:"ie"===c?b.gs[c]=b.gs[c]?1:0:!b.gs[c]&&delete b.gs[c]);for(var d in b)if(b.hasOwnProperty(d)&&!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&(a._Helpers.isObject(b[d][c])&&Object.keys(b[d][c]).length?b[d][c]=Base64.encode(JSON.stringify(b[d][c])):delete b[d][c]);return createCookieString(JSON.stringify(b))},this.decodePersistence=function(b){try{if(b){if(b=JSON.parse(revertCookieString(b)),a._Helpers.isObject(b)&&Object.keys(b).length){for(var c in b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]=JSON.parse(Base64.decode(b.gs[c])):"ie"===c&&(b.gs[c]=!!b.gs[c]));for(var d in b)if(b.hasOwnProperty(d))if(!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&b[d][c].length&&(b[d][c]=JSON.parse(Base64.decode(b[d][c])));else "l"===d&&(b[d]=!!b[d]);}return JSON.stringify(b)}}catch(b){a.Logger.error("Problem with decoding cookie",b);}},this.getCookieDomain=function(){if(a._Store.SDKConfig.cookieDomain)return a._Store.SDKConfig.cookieDomain;var b=f.getDomain(document,location.hostname);return ""===b?"":"."+b},this.getDomain=function(a,b){var c,d,e=b.split(".");for(c=e.length-1;0<=c;c--)if(d=e.slice(c).join("."),a.cookie="mptest=cookie;domain=."+d+";",-1>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;ff);o++)n=h[o],b[n]?(a.Logger.verb
if(d[g].mpid){var h=d[g].ui;for(var i in h)if(e===i&&b.userIdentities[e]===h[i]){c=g;break}}c&&f.storeDataInMemory(d,c);},this.encodePersistence=function(b){for(var c in b=JSON.parse(b),b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]?Array.isArray(b.gs[c])&&b.gs[c].length||a._Helpers.isObject(b.gs[c])&&Object.keys(b.gs[c]).length?b.gs[c]=Base64.encode(JSON.stringify(b.gs[c])):delete b.gs[c]:delete b.gs[c]:"ie"===c?b.gs[c]=b.gs[c]?1:0:!b.gs[c]&&delete b.gs[c]);for(var d in b)if(b.hasOwnProperty(d)&&!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&(a._Helpers.isObject(b[d][c])&&Object.keys(b[d][c]).length?b[d][c]=Base64.encode(JSON.stringify(b[d][c])):delete b[d][c]);return createCookieString(JSON.stringify(b))},this.decodePersistence=function(b){try{if(b){if(b=JSON.parse(revertCookieString(b)),a._Helpers.isObject(b)&&Object.keys(b).length){for(var c in b.gs)b.gs.hasOwnProperty(c)&&(Base64CookieKeys[c]?b.gs[c]=JSON.parse(Base64.decode(b.gs[c])):"ie"===c&&(b.gs[c]=!!b.gs[c]));for(var d in b)if(b.hasOwnProperty(d))if(!SDKv2NonMPIDCookieKeys[d])for(c in b[d])b[d].hasOwnProperty(c)&&Base64CookieKeys[c]&&b[d][c].length&&(b[d][c]=JSON.parse(Base64.decode(b[d][c])));else "l"===d&&(b[d]=!!b[d]);}return JSON.stringify(b)}}catch(b){a.Logger.error("Problem with decoding cookie",b);}},this.getCookieDomain=function(){if(a._Store.SDKConfig.cookieDomain)return a._Store.SDKConfig.cookieDomain;var b=f.getDomain(document,location.hostname);return ""===b?"":"."+b},this.getDomain=function(a,b){var c,d,e=b.split(".");for(c=e.length-1;0<=c;c--)if(d=e.slice(c).join("."),a.cookie="mptest=cookie;domain=."+d+";",-1
Date: Mon, 10 Nov 2025 14:55:23 +0000
Subject: [PATCH 05/11] chore(release): 2.49.0 [skip ci]
# [2.49.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.48.0...v2.49.0) (2025-11-10)
### Features
* add BrowserStack beta suite and workflow ([#1074](https://github.com/mParticle/mparticle-web-sdk/issues/1074)) ([8c5749d](https://github.com/mParticle/mparticle-web-sdk/commit/8c5749d42bdfe3c4a2ef0ca8e58d17705132218e))
---
CHANGELOG.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29eb7e39d..4d5b0ea78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [2.49.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.48.0...v2.49.0) (2025-11-10)
+
+
+### Features
+
+* add BrowserStack beta suite and workflow ([#1074](https://github.com/mParticle/mparticle-web-sdk/issues/1074)) ([8c5749d](https://github.com/mParticle/mparticle-web-sdk/commit/8c5749d42bdfe3c4a2ef0ca8e58d17705132218e))
+
# [2.48.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.47.1...v2.48.0) (2025-10-27)
diff --git a/package-lock.json b/package-lock.json
index 745ade2a3..da8b945af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@mparticle/web-sdk",
- "version": "2.48.0",
+ "version": "2.49.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@mparticle/web-sdk",
- "version": "2.48.0",
+ "version": "2.49.0",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.23.2"
diff --git a/package.json b/package.json
index e082f135a..9d7f371d1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@mparticle/web-sdk",
- "version": "2.48.0",
+ "version": "2.49.0",
"description": "mParticle core SDK for web applications",
"license": "Apache-2.0",
"keywords": [
From 3fb8cb123e6213d6a832a9455f5285e1c9989271 Mon Sep 17 00:00:00 2001
From: Robert Ing
Date: Wed, 12 Nov 2025 11:04:48 -0500
Subject: [PATCH 06/11] feat: SDKE-221 Support hashedEmailUserIdentityType for
"other" identity type (#1052)
---
src/roktManager.ts | 61 ++++++-
test/jest/roktManager.spec.ts | 306 ++++++++++++++++++++++++++++++++++
2 files changed, 361 insertions(+), 6 deletions(-)
diff --git a/src/roktManager.ts b/src/roktManager.ts
index bdb8089e4..4a9d48e6f 100644
--- a/src/roktManager.ts
+++ b/src/roktManager.ts
@@ -8,10 +8,13 @@ import {
generateUniqueId,
isFunction,
AttributeValue,
+ isEmpty,
} from "./utils";
import { SDKIdentityApi } from "./identity.interfaces";
import { SDKLoggerApi } from "./sdkRuntimeModels";
import { IStore, LocalSessionAttributes } from "./store";
+import { UserIdentities } from "@mparticle/web-sdk";
+import { IdentityType } from "./types";
// https://docs.rokt.com/developers/integration-guides/web/library/attributes
export interface IRoktPartnerAttributes {
@@ -96,6 +99,7 @@ export default class RoktManager {
private launcherOptions?: IRoktLauncherOptions;
private logger: SDKLoggerApi;
private domain?: string;
+ private mappedEmailShaIdentityType?: string | null;
/**
* Initializes the RoktManager with configuration settings and user data.
*
@@ -116,7 +120,8 @@ export default class RoktManager {
options?: IRoktOptions
): void {
const { userAttributeFilters, settings } = roktConfig || {};
- const { placementAttributesMapping } = settings || {};
+ const { placementAttributesMapping, hashedEmailUserIdentityType } = settings || {};
+ this.mappedEmailShaIdentityType = hashedEmailUserIdentityType?.toLowerCase() ?? null;
this.identityService = identityService;
this.store = store;
@@ -182,23 +187,43 @@ export default class RoktManager {
// Get current user identities
this.currentUser = this.identityService.getCurrentUser();
const currentUserIdentities = this.currentUser?.getUserIdentities()?.userIdentities || {};
+
const currentEmail = currentUserIdentities.email;
const newEmail = mappedAttributes.email as string;
- // https://go.mparticle.com/work/SQDSDKS-7338
- // Check if email exists and differs
- if (newEmail && (!currentEmail || currentEmail !== newEmail)) {
- if (currentEmail && currentEmail !== newEmail) {
+ let currentHashedEmail: string | undefined;
+ let newHashedEmail: string | undefined;
+
+ // Hashed email identity is valid if it is set to Other-Other10
+ if(this.mappedEmailShaIdentityType && IdentityType.getIdentityType(this.mappedEmailShaIdentityType) !== false) {
+ currentHashedEmail = currentUserIdentities[this.mappedEmailShaIdentityType];
+ newHashedEmail = mappedAttributes['emailsha256'] as string || mappedAttributes[this.mappedEmailShaIdentityType] as string || undefined;
+ }
+
+ const emailChanged = this.hasIdentityChanged(currentEmail, newEmail);
+ const hashedEmailChanged = this.hasIdentityChanged(currentHashedEmail, newHashedEmail);
+
+ const newIdentities: UserIdentities = {};
+ if (emailChanged) {
+ newIdentities.email = newEmail;
+ if (newEmail) {
this.logger.warning(`Email mismatch detected. Current email, ${currentEmail} differs from email passed to selectPlacements call, ${newEmail}. Proceeding to call identify with ${newEmail}. Please verify your implementation.`);
}
+ }
+
+ if (hashedEmailChanged) {
+ newIdentities[this.mappedEmailShaIdentityType] = newHashedEmail;
+ this.logger.warning(`emailsha256 mismatch detected. Current mParticle ${this.mappedEmailShaIdentityType} identity, ${currentHashedEmail}, differs from 'emailsha256' passed to selectPlacements call, ${newHashedEmail}. Proceeding to call identify with ${this.mappedEmailShaIdentityType} set to ${newHashedEmail}. Please verify your implementation`);
+ }
+ if (!isEmpty(newIdentities)) {
// Call identify with the new user identities
try {
await new Promise((resolve, reject) => {
this.identityService.identify({
userIdentities: {
...currentUserIdentities,
- email: newEmail
+ ...newIdentities
}
}, () => {
resolve();
@@ -321,6 +346,7 @@ export default class RoktManager {
this.messageQueue.forEach((message) => {
if(!(message.methodName in this) || !isFunction(this[message.methodName])) {
this.logger?.error(`RoktManager: Method ${message.methodName} not found`);
+
return;
}
@@ -373,4 +399,27 @@ export default class RoktManager {
this.messageQueue.delete(messageId);
}
+
+ /**
+ * Checks if an identity value has changed by comparing current and new values
+ *
+ * @param {string | undefined} currentValue - The current identity value
+ * @param {string | undefined} newValue - The new identity value to compare against
+ * @returns {boolean} True if the identity has changed (new value exists and differs from current), false otherwise
+ */
+ private hasIdentityChanged(currentValue: string | undefined, newValue: string | undefined): boolean {
+ if (!newValue) {
+ return false;
+ }
+
+ if (!currentValue) {
+ return true; // New value exists but no current value
+ }
+
+ if (currentValue !== newValue) {
+ return true; // Values are different
+ }
+
+ return false; // Values are the same
+ }
}
diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts
index 3c3bccae2..58beeedc3 100644
--- a/test/jest/roktManager.spec.ts
+++ b/test/jest/roktManager.spec.ts
@@ -364,6 +364,17 @@ describe('RoktManager', () => {
);
expect(roktManager['domain']).toBe(domain);
});
+
+ it('should set mappedEmailShaIdentityType as a lowercase hashedEmailUserIdentityType when passed as a setting', () => {
+ roktManager.init(
+ {settings: {hashedEmailUserIdentityType: 'Other5'}} as unknown as IKitConfigs,
+ undefined,
+ mockMPInstance.Identity,
+ mockMPInstance._Store,
+ mockMPInstance.Logger,
+ );
+ expect(roktManager['mappedEmailShaIdentityType']).toBe('other5');
+ });
});
describe('#attachKit', () => {
@@ -554,6 +565,7 @@ describe('RoktManager', () => {
describe('#selectPlacements', () => {
beforeEach(() => {
roktManager['currentUser'] = currentUser;
+ jest.clearAllMocks();
});
it('should call kit.selectPlacements with empty attributes', () => {
@@ -1110,6 +1122,234 @@ describe('RoktManager', () => {
email: 'new@example.com'
}
}, expect.any(Function));
+ expect(mockMPInstance.Logger.warning).toHaveBeenCalled();
+ });
+
+ it('should not call identify when user has current email but no email is passed to selectPlacements', async () => {
+ const kit: Partial = {
+ launcher: {
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ use: jest.fn(),
+ },
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ setExtensionData: jest.fn(),
+ };
+
+ roktManager['placementAttributesMapping'] = [];
+ roktManager.kit = kit as IRoktKit;
+
+ const mockIdentity = {
+ getCurrentUser: jest.fn().mockReturnValue({
+ getUserIdentities: () => ({
+ userIdentities: {
+ email: 'existing@example.com'
+ }
+ }),
+ setUserAttributes: jest.fn()
+ }),
+ identify: jest.fn().mockImplementation((data, callback) => {
+ // Call callback with no error to simulate success
+ callback();
+ })
+ } as unknown as SDKIdentityApi;
+
+ roktManager['identityService'] = mockIdentity;
+
+ const options: IRoktSelectPlacementsOptions = {
+ attributes: {
+ // No email attribute passed
+ // customAttribute: 'some-value'
+ }
+ };
+
+ await roktManager.selectPlacements(options);
+
+ expect(mockIdentity.identify).not.toHaveBeenCalled();
+ expect(mockMPInstance.Logger.warning).not.toHaveBeenCalled();
+ });
+
+ it('should call identify with emailsha256 mapped to other5 when it differs from current user other5 identity', async () => {
+ const kit: Partial = {
+ launcher: {
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ use: jest.fn(),
+ },
+ selectPlacements: jest.fn().mockResolvedValue({}),
+ hashAttributes: jest.fn(),
+ setExtensionData: jest.fn(),
+ };
+
+ roktManager.kit = kit as IRoktKit;
+ roktManager['mappedEmailShaIdentityType'] ='other5';
+
+ // Set up fresh mocks for this test
+ const mockIdentity = {
+ getCurrentUser: jest.fn().mockReturnValue({
+ getUserIdentities: () => ({
+ userIdentities: {
+ other5: 'old-other-value'
+ }
+ }),
+ setUserAttributes: jest.fn()
+ }),
+ identify: jest.fn().mockImplementation((data, callback) => {
+ // Call callback with no error to simulate success
+ callback();
+ })
+ } as unknown as SDKIdentityApi;
+
+ roktManager['identityService'] = mockIdentity;
+
+ const options: IRoktSelectPlacementsOptions = {
+ attributes: {
+ emailsha256: 'new-emailsha256-value'
+ }
+ };
+
+ await roktManager.selectPlacements(options);
+
+ expect(mockIdentity.identify).toHaveBeenCalledWith({
+ userIdentities: {
+ other5: 'new-emailsha256-value'
+ }
+ }, expect.any(Function));
+ expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith(
+ "emailsha256 mismatch detected. Current mParticle other5 identity, old-other-value, differs from 'emailsha256' passed to selectPlacements call, new-emailsha256-value. Proceeding to call identify with other5 set to new-emailsha256-value. Please verify your implementation"
+ );
+ });
+
+ it('should not call identify when emailsha256 matches current user other5 identity', () => {
+ const kit: Partial = {
+ launcher: {
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ use: jest.fn(),
+ },
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ setExtensionData: jest.fn(),
+ };
+
+ roktManager.kit = kit as IRoktKit;
+ roktManager['mappedEmailShaIdentityType'] = 'other5';
+
+ // Set up fresh mocks for this test
+ const mockIdentity = {
+ getCurrentUser: jest.fn().mockReturnValue({
+ getUserIdentities: () => ({
+ userIdentities: {
+ other5: 'same-emailsha256-value'
+ }
+ }),
+ setUserAttributes: jest.fn()
+ }),
+ identify: jest.fn()
+ };
+
+ roktManager['identityService'] = mockIdentity as unknown as SDKIdentityApi;
+
+ const options: IRoktSelectPlacementsOptions = {
+ attributes: {
+ emailsha256: 'same-emailsha256-value'
+ }
+ };
+
+ roktManager.selectPlacements(options);
+
+ expect(mockIdentity.identify).not.toHaveBeenCalled();
+ expect(mockMPInstance.Logger.warning).not.toHaveBeenCalled();
+ });
+
+ it('should call identify with emailsha256 mapped to other when current user has no other identity', async () => {
+ const kit: Partial = {
+ launcher: {
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ use: jest.fn(),
+ },
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ setExtensionData: jest.fn(),
+ };
+
+ roktManager.kit = kit as IRoktKit;
+ roktManager['mappedEmailShaIdentityType'] = 'other';
+
+ const mockIdentity = {
+ getCurrentUser: jest.fn().mockReturnValue({
+ getUserIdentities: () => ({
+ userIdentities: {}
+ }),
+ setUserAttributes: jest.fn()
+ }),
+ identify: jest.fn().mockImplementation((data, callback) => {
+ // Call callback with no error to simulate success
+ callback();
+ })
+ } as unknown as SDKIdentityApi;
+
+ roktManager['identityService'] = mockIdentity;
+
+ const options: IRoktSelectPlacementsOptions = {
+ attributes: {
+ emailsha256: 'new-emailsha256-value'
+ }
+ };
+
+ await roktManager.selectPlacements(options);
+
+ expect(mockIdentity.identify).toHaveBeenCalledWith({
+ userIdentities: {
+ other: 'new-emailsha256-value'
+ }
+ }, expect.any(Function));
+ expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith(
+ "emailsha256 mismatch detected. Current mParticle other identity, undefined, differs from 'emailsha256' passed to selectPlacements call, new-emailsha256-value. Proceeding to call identify with other set to new-emailsha256-value. Please verify your implementation"
+ );
+ });
+
+ it('should not call identify when current user has other identity but emailsha256 is null', () => {
+ const kit: Partial = {
+ launcher: {
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ use: jest.fn(),
+ },
+ selectPlacements: jest.fn(),
+ hashAttributes: jest.fn(),
+ setExtensionData: jest.fn(),
+ };
+
+ roktManager.kit = kit as IRoktKit;
+ roktManager['hashedEmailUserIdentityType'] = 'Other';
+
+ // Set up fresh mocks for this test
+ const mockIdentity = {
+ getCurrentUser: jest.fn().mockReturnValue({
+ getUserIdentities: () => ({
+ userIdentities: {
+ other: 'existing-other-value'
+ }
+ }),
+ setUserAttributes: jest.fn()
+ }),
+ identify: jest.fn()
+ };
+
+ roktManager['identityService'] = mockIdentity as unknown as SDKIdentityApi;
+
+ const options: IRoktSelectPlacementsOptions = {
+ attributes: {
+ // emailsha256 is not provided (null/undefined)
+ }
+ };
+
+ roktManager.selectPlacements(options);
+
+ expect(mockIdentity.identify).not.toHaveBeenCalled();
expect(mockMPInstance.Logger.warning).not.toHaveBeenCalled();
});
@@ -1463,4 +1703,70 @@ describe('RoktManager', () => {
});
});
+ describe('#hasIdentityChanged', () => {
+ it('should return false when newValue is null', () => {
+ const result = roktManager['hasIdentityChanged']('current@example.com', null);
+ expect(result).toBe(false);
+ });
+
+ it('should return false when newValue is undefined', () => {
+ const result = roktManager['hasIdentityChanged']('current@example.com', undefined);
+ expect(result).toBe(false);
+ });
+
+ it('should return false when newValue is empty string', () => {
+ const result = roktManager['hasIdentityChanged']('current@example.com', '');
+ expect(result).toBe(false);
+ });
+
+ it('should return true when currentValue is null and newValue exists', () => {
+ const result = roktManager['hasIdentityChanged'](null, 'new@example.com');
+ expect(result).toBe(true);
+ });
+
+ it('should return true when currentValue is undefined and newValue exists', () => {
+ const result = roktManager['hasIdentityChanged'](undefined, 'new@example.com');
+ expect(result).toBe(true);
+ });
+
+ it('should return true when currentValue is empty string and newValue exists', () => {
+ const result = roktManager['hasIdentityChanged']('', 'new@example.com');
+ expect(result).toBe(true);
+ });
+
+ it('should return true when currentValue and newValue are different', () => {
+ const result = roktManager['hasIdentityChanged']('old@example.com', 'new@example.com');
+ expect(result).toBe(true);
+ });
+
+ it('should return false when currentValue and newValue are the same', () => {
+ const result = roktManager['hasIdentityChanged']('same@example.com', 'same@example.com');
+ expect(result).toBe(false);
+ });
+
+ it('should return false when both currentValue and newValue are null', () => {
+ const result = roktManager['hasIdentityChanged'](null, null);
+ expect(result).toBe(false);
+ });
+
+ it('should return false when both currentValue and newValue are undefined', () => {
+ const result = roktManager['hasIdentityChanged'](undefined, undefined);
+ expect(result).toBe(false);
+ });
+
+ it('should return false when both currentValue and newValue are empty strings', () => {
+ const result = roktManager['hasIdentityChanged']('', '');
+ expect(result).toBe(false);
+ });
+
+ it('should handle whitespace-only strings as valid values', () => {
+ const result = roktManager['hasIdentityChanged']('old@example.com', ' ');
+ expect(result).toBe(true);
+ });
+
+ it('should be case sensitive', () => {
+ const result = roktManager['hasIdentityChanged']('test@example.com', 'TEST@EXAMPLE.COM');
+ expect(result).toBe(true);
+ });
+ });
});
\ No newline at end of file
From 31c9d3622b91f633ba8bcfd304253bddfa0025bd Mon Sep 17 00:00:00 2001
From: Robert Ing
Date: Thu, 13 Nov 2025 09:13:08 -0500
Subject: [PATCH 07/11] refactor: SDKE-560 Refactor logs for email mismatches
(#1114)
---
src/roktManager.ts | 4 ++--
test/jest/roktManager.spec.ts | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/roktManager.ts b/src/roktManager.ts
index 4a9d48e6f..0039d7d35 100644
--- a/src/roktManager.ts
+++ b/src/roktManager.ts
@@ -207,13 +207,13 @@ export default class RoktManager {
if (emailChanged) {
newIdentities.email = newEmail;
if (newEmail) {
- this.logger.warning(`Email mismatch detected. Current email, ${currentEmail} differs from email passed to selectPlacements call, ${newEmail}. Proceeding to call identify with ${newEmail}. Please verify your implementation.`);
+ this.logger.warning(`Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation.`);
}
}
if (hashedEmailChanged) {
newIdentities[this.mappedEmailShaIdentityType] = newHashedEmail;
- this.logger.warning(`emailsha256 mismatch detected. Current mParticle ${this.mappedEmailShaIdentityType} identity, ${currentHashedEmail}, differs from 'emailsha256' passed to selectPlacements call, ${newHashedEmail}. Proceeding to call identify with ${this.mappedEmailShaIdentityType} set to ${newHashedEmail}. Please verify your implementation`);
+ this.logger.warning(`emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation.`);
}
if (!isEmpty(newIdentities)) {
diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts
index 58beeedc3..69085d045 100644
--- a/test/jest/roktManager.spec.ts
+++ b/test/jest/roktManager.spec.ts
@@ -1030,7 +1030,7 @@ describe('RoktManager', () => {
}
}, expect.any(Function));
expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith(
- 'Email mismatch detected. Current email, old@example.com differs from email passed to selectPlacements call, new@example.com. Proceeding to call identify with new@example.com. Please verify your implementation.'
+ 'Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation.'
);
});
@@ -1217,7 +1217,7 @@ describe('RoktManager', () => {
}
}, expect.any(Function));
expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith(
- "emailsha256 mismatch detected. Current mParticle other5 identity, old-other-value, differs from 'emailsha256' passed to selectPlacements call, new-emailsha256-value. Proceeding to call identify with other5 set to new-emailsha256-value. Please verify your implementation"
+ "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation."
);
});
@@ -1307,7 +1307,7 @@ describe('RoktManager', () => {
}
}, expect.any(Function));
expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith(
- "emailsha256 mismatch detected. Current mParticle other identity, undefined, differs from 'emailsha256' passed to selectPlacements call, new-emailsha256-value. Proceeding to call identify with other set to new-emailsha256-value. Please verify your implementation"
+ "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation."
);
});
From 9acb42f1c3479b55394b9bfcff6c64681e102f94 Mon Sep 17 00:00:00 2001
From: mparticle-automation
Date: Thu, 13 Nov 2025 14:27:25 +0000
Subject: [PATCH 08/11] chore(build): Generate latest bundle [skip ci]
---
dist/mparticle.common.js | 11 ++++---
dist/mparticle.esm.js | 11 ++++---
dist/mparticle.js | 67 ++++++++++++++++++++++++++++++++--------
3 files changed, 66 insertions(+), 23 deletions(-)
diff --git a/dist/mparticle.common.js b/dist/mparticle.common.js
index 2246cab4b..0fa140b4e 100644
--- a/dist/mparticle.common.js
+++ b/dist/mparticle.common.js
@@ -15,7 +15,7 @@ map:function map(a,b){var c,d,e;if(null===this)throw new TypeError(" this is nul
filter:function filter(a/*, thisArg*/){if(void 0===this||null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;fAll of the following methods can be called on the primary mParticle class. In version 2.10.0, we introduced multiple instances. If you are using multiple instances (self hosted environments only), you should call these methods on each instance.
diff --git a/dist/mparticle.esm.js b/dist/mparticle.esm.js
index aa2b014e9..757b3a97c 100644
--- a/dist/mparticle.esm.js
+++ b/dist/mparticle.esm.js
@@ -15,7 +15,7 @@ map:function map(a,b){var c,d,e;if(null===this)throw new TypeError(" this is nul
filter:function filter(a/*, thisArg*/){if(void 0===this||null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;fAll of the following methods can be called on the primary mParticle class. In version 2.10.0, we introduced multiple instances. If you are using multiple instances (self hosted environments only), you should call these methods on each instance.
diff --git a/dist/mparticle.js b/dist/mparticle.js
index dedc2099e..0a29dcbf3 100644
--- a/dist/mparticle.js
+++ b/dist/mparticle.js
@@ -203,7 +203,7 @@ var mParticle = (function () {
Base64: Base64$1
};
- var version = "2.49.0";
+ var version = "2.50.0";
var Constants = {
sdkVersion: version,
@@ -9709,10 +9709,14 @@ var mParticle = (function () {
* @throws Logs error to console if placementAttributesMapping parsing fails
*/
RoktManager.prototype.init = function (roktConfig, filteredUser, identityService, store, logger, options) {
- var _a = roktConfig || {},
- userAttributeFilters = _a.userAttributeFilters,
- settings = _a.settings;
- var placementAttributesMapping = (settings || {}).placementAttributesMapping;
+ var _a;
+ var _b = roktConfig || {},
+ userAttributeFilters = _b.userAttributeFilters,
+ settings = _b.settings;
+ var _c = settings || {},
+ placementAttributesMapping = _c.placementAttributesMapping,
+ hashedEmailUserIdentityType = _c.hashedEmailUserIdentityType;
+ this.mappedEmailShaIdentityType = (_a = hashedEmailUserIdentityType === null || hashedEmailUserIdentityType === void 0 ? void 0 : hashedEmailUserIdentityType.toLowerCase()) !== null && _a !== void 0 ? _a : null;
this.identityService = identityService;
this.store = store;
this.logger = logger;
@@ -9762,7 +9766,7 @@ var mParticle = (function () {
RoktManager.prototype.selectPlacements = function (options) {
var _a, _b;
return __awaiter(this, void 0, void 0, function () {
- var attributes, sandboxValue, mappedAttributes, currentUserIdentities_1, currentEmail, newEmail_1, error_1, enrichedAttributes, enrichedOptions, error_2;
+ var attributes, sandboxValue, mappedAttributes, currentUserIdentities_1, currentEmail, newEmail, currentHashedEmail, newHashedEmail, emailChanged, hashedEmailChanged, newIdentities_1, error_1, enrichedAttributes, enrichedOptions, error_2;
var _this = this;
return __generator(this, function (_c) {
switch (_c.label) {
@@ -9780,19 +9784,34 @@ var mParticle = (function () {
this.currentUser = this.identityService.getCurrentUser();
currentUserIdentities_1 = ((_b = (_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.getUserIdentities()) === null || _b === void 0 ? void 0 : _b.userIdentities) || {};
currentEmail = currentUserIdentities_1.email;
- newEmail_1 = mappedAttributes.email;
- if (!(newEmail_1 && (!currentEmail || currentEmail !== newEmail_1))) return [3 /*break*/, 5];
- if (currentEmail && currentEmail !== newEmail_1) {
- this.logger.warning("Email mismatch detected. Current email, ".concat(currentEmail, " differs from email passed to selectPlacements call, ").concat(newEmail_1, ". Proceeding to call identify with ").concat(newEmail_1, ". Please verify your implementation."));
+ newEmail = mappedAttributes.email;
+ currentHashedEmail = void 0;
+ newHashedEmail = void 0;
+ // Hashed email identity is valid if it is set to Other-Other10
+ if (this.mappedEmailShaIdentityType && IdentityType.getIdentityType(this.mappedEmailShaIdentityType) !== false) {
+ currentHashedEmail = currentUserIdentities_1[this.mappedEmailShaIdentityType];
+ newHashedEmail = mappedAttributes['emailsha256'] || mappedAttributes[this.mappedEmailShaIdentityType] || undefined;
+ }
+ emailChanged = this.hasIdentityChanged(currentEmail, newEmail);
+ hashedEmailChanged = this.hasIdentityChanged(currentHashedEmail, newHashedEmail);
+ newIdentities_1 = {};
+ if (emailChanged) {
+ newIdentities_1.email = newEmail;
+ if (newEmail) {
+ this.logger.warning("Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation.");
+ }
+ }
+ if (hashedEmailChanged) {
+ newIdentities_1[this.mappedEmailShaIdentityType] = newHashedEmail;
+ this.logger.warning("emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation.");
}
+ if (!!isEmpty(newIdentities_1)) return [3 /*break*/, 5];
_c.label = 2;
case 2:
_c.trys.push([2, 4,, 5]);
return [4 /*yield*/, new Promise(function (resolve, reject) {
_this.identityService.identify({
- userIdentities: __assign(__assign({}, currentUserIdentities_1), {
- email: newEmail_1
- })
+ userIdentities: __assign(__assign({}, currentUserIdentities_1), newIdentities_1)
}, function () {
resolve();
});
@@ -9950,6 +9969,28 @@ var mParticle = (function () {
}
this.messageQueue["delete"](messageId);
};
+ /**
+ * Checks if an identity value has changed by comparing current and new values
+ *
+ * @param {string | undefined} currentValue - The current identity value
+ * @param {string | undefined} newValue - The new identity value to compare against
+ * @returns {boolean} True if the identity has changed (new value exists and differs from current), false otherwise
+ */
+ RoktManager.prototype.hasIdentityChanged = function (currentValue, newValue) {
+ if (!newValue) {
+ return false;
+ }
+ if (!currentValue) {
+ return true; // New value exists but no current value
+ }
+
+ if (currentValue !== newValue) {
+ return true; // Values are different
+ }
+
+ return false; // Values are the same
+ };
+
return RoktManager;
}();
From 3cb131931d39cbe36e3e452e719bfe9a0ca4723e Mon Sep 17 00:00:00 2001
From: mparticle-automation
Date: Thu, 13 Nov 2025 14:27:29 +0000
Subject: [PATCH 09/11] chore(release): 2.50.0 [skip ci]
# [2.50.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.49.0...v2.50.0) (2025-11-13)
### Features
* SDKE-221 Support hashedEmailUserIdentityType for "other" identity type ([#1052](https://github.com/mParticle/mparticle-web-sdk/issues/1052)) ([3fb8cb1](https://github.com/mParticle/mparticle-web-sdk/commit/3fb8cb123e6213d6a832a9455f5285e1c9989271))
---
CHANGELOG.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d5b0ea78..317eb37b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+# [2.50.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.49.0...v2.50.0) (2025-11-13)
+
+
+### Features
+
+* SDKE-221 Support hashedEmailUserIdentityType for "other" identity type ([#1052](https://github.com/mParticle/mparticle-web-sdk/issues/1052)) ([3fb8cb1](https://github.com/mParticle/mparticle-web-sdk/commit/3fb8cb123e6213d6a832a9455f5285e1c9989271))
+
# [2.49.0](https://github.com/mParticle/mparticle-web-sdk/compare/v2.48.0...v2.49.0) (2025-11-10)
diff --git a/package-lock.json b/package-lock.json
index da8b945af..44ed71906 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@mparticle/web-sdk",
- "version": "2.49.0",
+ "version": "2.50.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@mparticle/web-sdk",
- "version": "2.49.0",
+ "version": "2.50.0",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.23.2"
diff --git a/package.json b/package.json
index 9d7f371d1..174d62dc1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@mparticle/web-sdk",
- "version": "2.49.0",
+ "version": "2.50.0",
"description": "mParticle core SDK for web applications",
"license": "Apache-2.0",
"keywords": [
From e76cc774cbd67ca01fdeecdf23eb48ff7744d46a Mon Sep 17 00:00:00 2001
From: Jaissica
Date: Mon, 17 Nov 2025 10:22:07 -0500
Subject: [PATCH 10/11] add hashSha256 for hashing attributes and restore
hashAttributes functionality
---
src/roktManager.ts | 53 +++++++++++++++++
test/jest/roktManager.spec.ts | 106 ++++++++++++++++++++++++++++++++++
2 files changed, 159 insertions(+)
diff --git a/src/roktManager.ts b/src/roktManager.ts
index 0039d7d35..5b17381c4 100644
--- a/src/roktManager.ts
+++ b/src/roktManager.ts
@@ -264,6 +264,28 @@ export default class RoktManager {
}
}
+ /**
+ * Hashes a single value using SHA-256
+ * Accepts the same types as IRoktPartnerAttributes values
+ *
+ * @param {string | number | boolean | undefined | null} attribute - The value to hash
+ * @returns {Promise} The SHA-256 hex digest of the normalized value
+ *
+ */
+ public async hashSha256(attribute: string | number | boolean | undefined | null): Promise {
+ try {
+ if (attribute === undefined || attribute === null) {
+ return Promise.reject(new Error('Value cannot be null or undefined'));
+ }
+ const normalizedValue = String(attribute).trim().toLocaleLowerCase();
+ return await this.sha256Hex(normalizedValue);
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.logger.error('Failed hashSha256: ' + errorMessage);
+ return Promise.reject(new Error(String(error)));
+ }
+ }
+
public setExtensionData(extensionData: IRoktPartnerExtensionData): void {
if (!this.isReady()) {
this.deferredCall('setExtensionData', extensionData);
@@ -400,6 +422,37 @@ export default class RoktManager {
this.messageQueue.delete(messageId);
}
+ /**
+ * Hashes a string input using SHA-256 and returns the hex digest
+ * Uses the Web Crypto API for secure hashing
+ *
+ * @param {string} input - The string to hash
+ * @returns {Promise} The SHA-256 hash as a hexadecimal string
+ */
+ private async sha256Hex(input: string): Promise {
+ const encoder = new TextEncoder();
+ const encodedInput = encoder.encode(input);
+ const digest = await crypto.subtle.digest('SHA-256', encodedInput);
+ return this.arrayBufferToHex(digest);
+ }
+
+ /**
+ * Converts an ArrayBuffer to a hexadecimal string representation
+ * Each byte is converted to a 2-character hex string with leading zeros
+ *
+ * @param {ArrayBuffer} buffer - The buffer to convert
+ * @returns {string} The hexadecimal string representation
+ */
+ private arrayBufferToHex(buffer: ArrayBuffer): string {
+ const bytes = new Uint8Array(buffer);
+ let hexString = '';
+ for (let i = 0; i < bytes.length; i++) {
+ const hexByte = bytes[i].toString(16).padStart(2, '0');
+ hexString += hexByte;
+ }
+ return hexString;
+ }
+
/**
* Checks if an identity value has changed by comparing current and new values
*
diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts
index 69085d045..68dd4da55 100644
--- a/test/jest/roktManager.spec.ts
+++ b/test/jest/roktManager.spec.ts
@@ -210,6 +210,112 @@ describe('RoktManager', () => {
});
});
+ describe('#hashSha256', () => {
+ interface Hasher {
+ sha256Hex(input: string): Promise
+ }
+
+ const nodeCrypto = require('crypto');
+ let shaSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ shaSpy.mockImplementation((s: any) =>
+ Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')),
+ );
+ });
+
+ afterEach(() => {
+ shaSpy.mockRestore();
+ });
+
+ it('should hash a single string value using SHA-256', async () => {
+ const result = await roktManager.hashSha256('test@example.com');
+ const expected = nodeCrypto.createHash('sha256').update('test@example.com').digest('hex');
+
+ expect(result).toBe(expected);
+ expect(shaSpy).toHaveBeenCalledWith('test@example.com');
+ expect(shaSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('should hash values without kit being attached', async () => {
+ // Verify kit is not attached
+ expect(roktManager['kit']).toBeNull();
+
+ const result = await roktManager.hashSha256('user@example.com');
+ const expected = nodeCrypto.createHash('sha256').update('user@example.com').digest('hex');
+
+ expect(result).toBe(expected);
+ });
+
+ it('should handle empty string', async () => {
+ const emptyStringHash = await roktManager.hashSha256('');
+
+ // Empty string after trim becomes '', hash of empty string
+ expect(emptyStringHash).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
+ });
+
+ it('should reject when value is null', async () => {
+ await expect(roktManager.hashSha256(null)).rejects.toThrow('Value cannot be null or undefined');
+ });
+
+ it('should reject when value is undefined', async () => {
+ await expect(roktManager.hashSha256(undefined)).rejects.toThrow('Value cannot be null or undefined');
+ });
+
+ it('should log error when hashing fails', async () => {
+ shaSpy.mockRejectedValue(new Error('Hash failed'));
+
+ await expect(roktManager.hashSha256('test@example.com')).rejects.toThrow();
+ expect(mockMPInstance.Logger.error).toHaveBeenCalledWith(expect.stringContaining('Failed hashSha256'));
+ });
+
+ it('should hash firstName to known SHA-256 value', async () => {
+ const hashedFirstName = await roktManager.hashSha256('jane');
+
+ // Expected SHA-256 hash of 'jane'
+ expect(hashedFirstName).toBe('81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2');
+ });
+
+ it('should produce same hash for different case and whitespace variations', async () => {
+ const lowercaseEmail = await roktManager.hashSha256('jane.doe@gmail.com');
+ const mixedCaseEmail = await roktManager.hashSha256('Jane.Doe@gmail.com');
+ const emailWithWhitespace = await roktManager.hashSha256(' jane.doe@gmail.com ');
+
+ // All should normalize to same hash
+ expect(lowercaseEmail).toBe(mixedCaseEmail);
+ expect(mixedCaseEmail).toBe(emailWithWhitespace);
+ expect(lowercaseEmail).toBe('831f6494ad6be4fcb3a724c3d5fef22d3ceffa3c62ef3a7984e45a0ea177f982');
+ });
+
+ it('should handle numeric values and match known SHA-256', async () => {
+ const hashedNumber = await roktManager.hashSha256(42);
+ const hashedString = await roktManager.hashSha256('42');
+
+ // Numeric value should be converted to string and produce same hash
+ expect(hashedNumber).toBe(hashedString);
+ // Expected SHA-256 hash of '42'
+ expect(hashedNumber).toBe('73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049');
+ });
+
+ it('should handle boolean values and match known SHA-256', async () => {
+ const hashedBoolean = await roktManager.hashSha256(true);
+ const hashedString = await roktManager.hashSha256('true');
+
+ // Boolean value should be converted to string and produce same hash
+ expect(hashedBoolean).toBe(hashedString);
+ // Expected SHA-256 hash of 'true'
+ expect(hashedBoolean).toBe('b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b');
+ });
+
+ it('should hash phone number to known SHA-256 value', async () => {
+ const hashedPhone = await roktManager.hashSha256('1234567890');
+
+ // Expected SHA-256 hash of '1234567890'
+ expect(hashedPhone).toBe('c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646');
+ });
+ });
+
describe('#init', () => {
it('should initialize the manager with defaults when no config is provided', () => {
roktManager.init(
From 72b3e090d43826ef6f46db9fa1dc35a240e86830 Mon Sep 17 00:00:00 2001
From: Jaissica
Date: Mon, 3 Nov 2025 15:18:40 -0500
Subject: [PATCH 11/11] added tests for roktManager.hashAttributes in
roktManager.spec.ts
---
test/jest/roktManager.spec.ts | 196 ++++++++++++++++------------------
1 file changed, 94 insertions(+), 102 deletions(-)
diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts
index 68dd4da55..06818fe63 100644
--- a/test/jest/roktManager.spec.ts
+++ b/test/jest/roktManager.spec.ts
@@ -2,7 +2,7 @@ import { IKitConfigs } from "../../src/configAPIClient";
import { IMParticleUser } from "../../src/identity-user-interfaces";
import { SDKIdentityApi } from "../../src/identity.interfaces";
import { IMParticleWebSDKInstance } from "../../src/mp-instance";
-import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions } from "../../src/roktManager";
+import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions, IRoktPartnerAttributes, IRoktLauncher, IRoktSelection } from "../../src/roktManager";
import { testMPID } from '../src/config/constants';
const resolvePromise = () => new Promise(resolve => setTimeout(resolve, 0));
@@ -76,82 +76,68 @@ describe('RoktManager', () => {
});
describe('#hashAttributes', () => {
+ interface Hasher {
+ sha256Hex(input: string): Promise
+ }
+
beforeEach(() => {
roktManager['currentUser'] = currentUser;
});
- it('should call kit.hashAttributes with empty attributes', () => {
- const kit: IRoktKit = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
- filters: undefined,
- filteredUser: undefined,
- hashAttributes: jest.fn(),
- selectPlacements: jest.fn(),
- setExtensionData: jest.fn(),
- use: jest.fn(),
- userAttributes: undefined,
- };
-
- roktManager.attachKit(kit);
+ it('should not hash when calling roktManager.hashAttributes with empty attributes and no kit', async () => {
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ const attributes: IRoktPartnerAttributes = {};
+ const messageQueueSizeBefore = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ const messageQueueSizeAfter = roktManager['messageQueue'].size;
+ expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore);
+ expect(shaSpy).not.toHaveBeenCalled();
+ shaSpy.mockRestore();
+ });
- const attributes = {};
+ it('should hash when calling roktManager.hashAttributes without kit', async () => {
+ const nodeCrypto = require('crypto');
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ shaSpy.mockImplementation((s: any) =>
+ Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')),
+ );
- roktManager.hashAttributes(attributes);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
- });
+ const attributes: IRoktPartnerAttributes = { email: 'Jane.Doe@Gmail.com', phone: ' 1234567890 ' };
+ const messageQueueSizeBefore = roktManager['messageQueue'].size;
- it('should call kit.hashAttributes with passed in attributes', () => {
- const kit: IRoktKit = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
- filters: undefined,
- filteredUser: undefined,
- hashAttributes: jest.fn(),
- selectPlacements: jest.fn(),
- setExtensionData: jest.fn(),
- use: jest.fn(),
- userAttributes: undefined,
- };
+ await roktManager.hashAttributes(attributes);
- roktManager.attachKit(kit);
+ const messageQueueSizeAfter = roktManager['messageQueue'].size;
+ expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore);
- const attributes = {
- email: 'test@example.com',
- phone: '1234567890'
- };
+ expect(shaSpy).toHaveBeenCalledWith('jane.doe@gmail.com');
+ expect(shaSpy).toHaveBeenCalledWith('1234567890');
+ expect(shaSpy).toHaveBeenCalledTimes(2);
- roktManager.hashAttributes(attributes);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
+ shaSpy.mockRestore();
});
- it('should queue the hashAttributes method if no launcher or kit is attached', () => {
- const attributes = {
- email: 'test@example.com'
- };
+ it('should not queue when calling roktManager.hashAttributes with no kit attached', async () => {
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex').mockResolvedValue('deadbeef');
- roktManager.hashAttributes(attributes);
+ const attributes: IRoktPartnerAttributes = { email: 'test@example.com' };
+ const messageQueueSizeBefore = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ const messageQueueSizeAfter = roktManager['messageQueue'].size;
+ expect(messageQueueSizeAfter).toBe(messageQueueSizeBefore);
- expect(roktManager['kit']).toBeNull();
- expect(roktManager['messageQueue'].size).toBe(1);
- const queuedMessage = Array.from(roktManager['messageQueue'].values())[0];
- expect(queuedMessage.methodName).toBe('hashAttributes');
- expect(queuedMessage.payload).toBe(attributes);
+ shaSpy.mockRestore();
});
- it('should process queued hashAttributes calls once the launcher and kit are attached', () => {
+ it('should remain non-deferred before and after attaching kit', async () => {
+ const nodeCrypto = require('crypto');
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ shaSpy.mockImplementation((s: any) =>
+ Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')),
+ );
+
const kit: IRoktKit = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
+ launcher: { selectPlacements: jest.fn(), hashAttributes: jest.fn(), use: jest.fn() },
filters: undefined,
filteredUser: undefined,
hashAttributes: jest.fn(),
@@ -161,52 +147,55 @@ describe('RoktManager', () => {
userAttributes: undefined,
};
- const attributes = {
- email: 'test@example.com'
- };
+ const attributes: IRoktPartnerAttributes = { email: 'test@example.com' };
- roktManager.hashAttributes(attributes);
- expect(roktManager['kit']).toBeNull();
- expect(roktManager['messageQueue'].size).toBe(1);
- const queuedMessage = Array.from(roktManager['messageQueue'].values())[0];
- expect(queuedMessage.methodName).toBe('hashAttributes');
- expect(queuedMessage.payload).toBe(attributes);
- expect(kit.hashAttributes).not.toHaveBeenCalled();
+ const sizeBefore = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ expect(roktManager['messageQueue'].size).toBe(sizeBefore);
roktManager.attachKit(kit);
- expect(roktManager['kit']).not.toBeNull();
- expect(roktManager['messageQueue'].size).toBe(0);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
+ const sizeAfterAttach = roktManager['messageQueue'].size;
+ await roktManager.hashAttributes(attributes);
+ expect(roktManager['messageQueue'].size).toBe(sizeAfterAttach);
+
+ shaSpy.mockRestore();
});
- it('should pass through the correct attributes to kit.launcher.hashAttributes', async () => {
+ it('should allow calling launcher.hashAttributes directly when kit is attached', async () => {
+ const launcher: IRoktLauncher = {
+ selectPlacements: jest.fn().mockResolvedValue({ close: jest.fn(), getPlacements: jest.fn().mockResolvedValue([]) } as unknown as IRoktSelection),
+ hashAttributes: jest.fn(),
+ use: jest.fn().mockResolvedValue(undefined as unknown as never),
+ };
const kit: Partial = {
- launcher: {
- selectPlacements: jest.fn(),
- hashAttributes: jest.fn(),
- use: jest.fn(),
- },
-
- // We are mocking the hashAttributes method to return the
- // launcher's hashAttributes method and verify that
- // both the kit's and the launcher's methods
- // are called with the correct attributes.
- // This will happen through the Web Kit's hashAttributes method
- hashAttributes: jest.fn().mockImplementation((attributes) => {
- return kit.launcher.hashAttributes(attributes);
- })
+ launcher,
+ hashAttributes: jest.fn(),
};
-
roktManager.attachKit(kit as IRoktKit);
+ const attributes: IRoktPartnerAttributes = { email: 'test@example.com', phone: '1234567890' };
+ await launcher.hashAttributes(attributes);
+ expect(launcher.hashAttributes).toHaveBeenCalledWith(attributes);
+ });
- const attributes = {
- email: 'test@example.com',
- phone: '1234567890'
- };
-
- roktManager.hashAttributes(attributes);
- expect(kit.hashAttributes).toHaveBeenCalledWith(attributes);
- expect(kit.launcher.hashAttributes).toHaveBeenCalledWith(attributes);
+ it('should match node crypto output', async () => {
+ const nodeCrypto = require('crypto');
+ const shaSpy = jest.spyOn(roktManager as unknown as Hasher, 'sha256Hex');
+ shaSpy.mockImplementation((s: any) =>
+ Promise.resolve(nodeCrypto.createHash('sha256').update(String(s)).digest('hex')),
+ );
+ const attributes: IRoktPartnerAttributes = {
+ email: ' Jane.DOE@GMAIL.com ',
+ phone: ' 1234567890 ',
+ blank: ' ',
+ };
+ const fromManager = await roktManager.hashAttributes(attributes);
+ const expected = {
+ emailsha256: nodeCrypto.createHash('sha256').update('jane.doe@gmail.com').digest('hex'),
+ phonesha256: nodeCrypto.createHash('sha256').update('1234567890').digest('hex'),
+ blanksha256: nodeCrypto.createHash('sha256').update('').digest('hex'),
+ };
+ expect(fromManager).toStrictEqual(expected);
+ shaSpy.mockRestore();
});
});
@@ -543,7 +532,7 @@ describe('RoktManager', () => {
expect(kit.selectPlacements).toHaveBeenCalledTimes(3);
});
- it('should call RoktManager methods (not kit methods directly) when processing queue', () => {
+ it('should call RoktManager methods (not kit methods directly) when processing queue', async () => {
// Queue some calls before kit is ready (these will be deferred)
const selectOptions = { attributes: { test: 'value' } } as IRoktSelectPlacementsOptions;
const hashAttrs = { email: 'test@example.com' };
@@ -551,14 +540,12 @@ describe('RoktManager', () => {
const useName = 'TestExtension';
roktManager.selectPlacements(selectOptions);
- roktManager.hashAttributes(hashAttrs);
roktManager.setExtensionData(extensionData);
roktManager.use(useName);
// Verify calls were queued
- expect(roktManager['messageQueue'].size).toBe(4);
+ expect(roktManager['messageQueue'].size).toBe(3);
expect(kit.selectPlacements).not.toHaveBeenCalled(); // Kit methods not called yet
- expect(kit.hashAttributes).not.toHaveBeenCalled(); // Kit methods not called yet
expect(kit.setExtensionData).not.toHaveBeenCalled(); // Kit methods not called yet
expect(kit.use).not.toHaveBeenCalled(); // Kit methods not called yet
@@ -568,6 +555,8 @@ describe('RoktManager', () => {
const setExtensionDataSpy = jest.spyOn(roktManager, 'setExtensionData');
const useSpy = jest.spyOn(roktManager, 'use');
+ const shaSpy = jest.spyOn(roktManager as any, 'sha256Hex').mockResolvedValue('deadbeef');
+ await roktManager.hashAttributes(hashAttrs);
// Attach kit (triggers processMessageQueue)
roktManager.attachKit(kit);
@@ -577,7 +566,7 @@ describe('RoktManager', () => {
expect(hashAttributesSpy).toHaveBeenCalledTimes(1);
expect(hashAttributesSpy).toHaveBeenCalledWith(hashAttrs);
-
+
expect(setExtensionDataSpy).toHaveBeenCalledTimes(1);
expect(setExtensionDataSpy).toHaveBeenCalledWith(extensionData);
@@ -592,6 +581,7 @@ describe('RoktManager', () => {
hashAttributesSpy.mockRestore();
setExtensionDataSpy.mockRestore();
useSpy.mockRestore();
+ shaSpy.mockRestore();
});
it('should preserve RoktManager preprocessing logic when processing deferred selectPlacements calls', () => {
@@ -747,6 +737,7 @@ describe('RoktManager', () => {
});
it('should process queued selectPlacements calls once the launcher and kit are attached', async () => {
+ const shaSpy = jest.spyOn(roktManager as any, 'sha256Hex').mockResolvedValue('deadbeef');
const expectedResult = { placements: ['placement1', 'placement2'] };
const kit: IRoktKit = {
launcher: {
@@ -793,6 +784,7 @@ describe('RoktManager', () => {
expect(roktManager['messageQueue'].size).toBe(0);
expect(kit.selectPlacements).toHaveBeenCalledWith(options);
expect(result).toEqual(expectedResult);
+ shaSpy.mockRestore();
});
it('should pass through the correct attributes to kit.selectPlacements', () => {