From eae3d27f46ee962d5a547bd64b1a9aa0710399a6 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Mon, 17 Nov 2025 10:22:07 -0500 Subject: [PATCH 1/9] feat: Add hashSha256 method for SHA-256 hashing and related test --- 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 0039d7d3..5b17381c 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 69085d04..68dd4da5 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 5708ef0c8f0873d817af44a6975c4d6b105e3f58 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Mon, 17 Nov 2025 11:27:57 -0500 Subject: [PATCH 2/9] move hashSha256 method after rokt kit methods for better organization --- src/roktManager.ts | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index 5b17381c..f379d94c 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -264,28 +264,6 @@ 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); @@ -312,6 +290,28 @@ export default class RoktManager { } } + /** + * Hashes an attribute using SHA-256 + * + * @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 { + if (attribute === undefined || attribute === null) { + throw new Error('Value cannot be null or undefined'); + } + + try { + 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); + throw error; + } + } + public getLocalSessionAttributes(): LocalSessionAttributes { return this.store.getLocalSessionAttributes(); } From 1098eee65af22e2f3efe88046c4a18acf43b8cd5 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Wed, 19 Nov 2025 17:42:06 -0500 Subject: [PATCH 3/9] add logger warning for null/undefined inputs in hashSha256 --- src/roktManager.ts | 11 ++++++----- test/jest/roktManager.spec.ts | 16 +++++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index f379d94c..e02316d9 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -294,12 +294,13 @@ export default class RoktManager { * Hashes an attribute using SHA-256 * * @param {string | number | boolean | undefined | null} attribute - The value to hash - * @returns {Promise} The SHA-256 hex digest of the normalized value + * @returns {Promise} SHA-256 hashed value or undefined/null * */ - public async hashSha256(attribute: string | number | boolean | undefined | null): Promise { - if (attribute === undefined || attribute === null) { - throw new Error('Value cannot be null or undefined'); + public async hashSha256(attribute: string | number | boolean | undefined | null): Promise { + if (attribute === null || attribute === undefined) { + this.logger.warning(`hashSha256 received ${attribute} as input`); + return attribute as null | undefined; } try { @@ -307,7 +308,7 @@ export default class RoktManager { return await this.sha256Hex(normalizedValue); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error('Failed hashSha256: ' + errorMessage); + this.logger.error('Failed to hash attribute: ' + errorMessage); throw error; } } diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 68dd4da5..5db1f45e 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -255,19 +255,25 @@ describe('RoktManager', () => { 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 return null and log warning when value is null', async () => { + const result = await roktManager.hashSha256(null); + + expect(result).toBeNull(); + expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith('hashSha256 received null as input'); }); - it('should reject when value is undefined', async () => { - await expect(roktManager.hashSha256(undefined)).rejects.toThrow('Value cannot be null or undefined'); + it('should return undefined and log warning when value is undefined', async () => { + const result = await roktManager.hashSha256(undefined); + + expect(result).toBeUndefined(); + expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith('hashSha256 received undefined as input'); }); 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')); + expect(mockMPInstance.Logger.error).toHaveBeenCalledWith(expect.stringContaining('Failed to hash attribute')); }); it('should hash firstName to known SHA-256 value', async () => { From a78419ebfaf3e4a7858a4b9b589249cd85f0e07f Mon Sep 17 00:00:00 2001 From: Jaissica Date: Thu, 20 Nov 2025 12:03:22 -0500 Subject: [PATCH 4/9] return undefined instead of throwing when hashSha256 fails --- src/roktManager.ts | 4 ++-- test/jest/roktManager.spec.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index e02316d9..77267c95 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -308,8 +308,8 @@ export default class RoktManager { return await this.sha256Hex(normalizedValue); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error('Failed to hash attribute: ' + errorMessage); - throw error; + this.logger.error(`Failed to hash "${attribute}" and returning undefined, selectPlacements will continue: ${errorMessage}`); + return undefined; } } diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 5db1f45e..a3caf82e 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -269,11 +269,15 @@ describe('RoktManager', () => { expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith('hashSha256 received undefined as input'); }); - it('should log error when hashing fails', async () => { + it('should return undefined and 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 to hash attribute')); + const result = await roktManager.hashSha256('test@example.com'); + + expect(result).toBeUndefined(); + expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to hash "test@example.com" and returning undefined, selectPlacements will continue') + ); }); it('should hash firstName to known SHA-256 value', async () => { From 8c91a8b5048da39c5277083bae5877c4d8f29f14 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Mon, 24 Nov 2025 15:04:18 -0500 Subject: [PATCH 5/9] Update hashAttributes to use hashSha256 internally for hashing and update tests --- src/roktManager.ts | 38 +++++-- test/jest/roktManager.spec.ts | 184 +++++++++++----------------------- 2 files changed, 92 insertions(+), 130 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index 77267c95..57a6216f 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -252,15 +252,39 @@ export default class RoktManager { } } - public hashAttributes(attributes: IRoktPartnerAttributes): Promise { - if (!this.isReady()) { - return this.deferredCall('hashAttributes', attributes); - } - + /** + * Hashes attributes and returns both original and hashed versions + * with Rokt-compatible key names (like emailsha256, mobilesha256) + * + * + * @param {IRoktPartnerAttributes} attributes - Attributes to hash + * @returns {Promise} Object with both original and hashed attributes + * + */ + public async hashAttributes(attributes: IRoktPartnerAttributes): Promise { try { - return this.kit.hashAttributes(attributes); + if (!attributes || typeof attributes !== 'object') { + return {}; + } + + const hashedAttributes: IRoktPartnerAttributes = {}; + + for (const key in attributes) { + const attributeValue = attributes[key]; + + hashedAttributes[key] = attributeValue; + + const hashedValue = await this.hashSha256(attributeValue); + + if (hashedValue) { + hashedAttributes[`${key}sha256`] = hashedValue; + } + } + + return hashedAttributes; + } catch (error) { - return Promise.reject(error instanceof Error ? error : new Error('Unknown error occurred')); + return Promise.reject(error instanceof Error ? error : new Error('Failed to hash attributes')); } } diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index a3caf82e..6f9ef3eb 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -76,152 +76,98 @@ describe('RoktManager', () => { }); describe('#hashAttributes', () => { + const nodeCrypto = require('crypto'); + let shaSpy: jest.SpyInstance; + beforeEach(() => { roktManager['currentUser'] = currentUser; + shaSpy = jest.spyOn(roktManager as any, 'sha256Hex'); + shaSpy.mockImplementation((s: string) => + Promise.resolve(nodeCrypto.createHash('sha256').update(s).digest('hex')), + ); }); - 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); - - const attributes = {}; - - roktManager.hashAttributes(attributes); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); + afterEach(() => { + shaSpy.mockRestore(); }); - 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, - }; - - roktManager.attachKit(kit); - + it('should hash attributes with passed in attributes', async () => { const attributes = { email: 'test@example.com', phone: '1234567890' }; - roktManager.hashAttributes(attributes); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); + const result = await roktManager.hashAttributes(attributes); + + expect(result).toEqual({ + email: 'test@example.com', + emailsha256: nodeCrypto.createHash('sha256').update('test@example.com').digest('hex'), + phone: '1234567890', + phonesha256: nodeCrypto.createHash('sha256').update('1234567890').digest('hex'), + }); }); - it('should queue the hashAttributes method if no launcher or kit is attached', () => { + it('should hash all non-null string, number, and boolean values', async () => { const attributes = { - email: 'test@example.com' + email: 'Test@Example.COM', + age: 25, + active: true, + nullable: null, + undefinedVal: undefined }; - roktManager.hashAttributes(attributes); + const result = await 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(result).toEqual({ + email: 'Test@Example.COM', + emailsha256: nodeCrypto.createHash('sha256').update('test@example.com').digest('hex'), + age: 25, + agesha256: nodeCrypto.createHash('sha256').update('25').digest('hex'), + active: true, + activesha256: nodeCrypto.createHash('sha256').update('true').digest('hex'), + nullable: null, + undefinedVal: undefined + }); }); - it('should process queued hashAttributes calls once the launcher and kit are attached', () => { - 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, - }; - - const attributes = { - 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(); - - roktManager.attachKit(kit); - expect(roktManager['kit']).not.toBeNull(); - expect(roktManager['messageQueue'].size).toBe(0); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); + it('should return empty object if attributes is null', async () => { + const result = await roktManager.hashAttributes(null as any); + expect(result).toEqual({}); }); - it('should pass through the correct attributes to kit.launcher.hashAttributes', async () => { - const kit: Partial = { - launcher: { - selectPlacements: jest.fn(), - hashAttributes: jest.fn(), - use: jest.fn(), - }, + it('should return empty object if attributes is undefined', async () => { + const result = await roktManager.hashAttributes(undefined as any); + expect(result).toEqual({}); + }); - // 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); - }) - }; + it('should handle empty attributes object', async () => { + const result = await roktManager.hashAttributes({}); + expect(result).toEqual({}); + }); - roktManager.attachKit(kit as IRoktKit); + it('should reject if hashSha256 throws an error', async () => { + shaSpy.mockRestore(); + + // Mock hashSha256 to throw an error + const hashError = new Error('Hashing failed'); + jest.spyOn(roktManager, 'hashSha256').mockRejectedValue(hashError); - const attributes = { - email: 'test@example.com', - phone: '1234567890' - }; + const attributes = { email: 'test@example.com' }; - roktManager.hashAttributes(attributes); - expect(kit.hashAttributes).toHaveBeenCalledWith(attributes); - expect(kit.launcher.hashAttributes).toHaveBeenCalledWith(attributes); + await expect(roktManager.hashAttributes(attributes)) + .rejects + .toThrow('Hashing failed'); }); }); 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')), + shaSpy = jest.spyOn(roktManager as any, 'sha256Hex'); + shaSpy.mockImplementation((s: string) => + Promise.resolve(nodeCrypto.createHash('sha256').update(s).digest('hex')), ); }); @@ -556,25 +502,21 @@ describe('RoktManager', () => { it('should call RoktManager methods (not kit methods directly) when processing queue', () => { // Queue some calls before kit is ready (these will be deferred) const selectOptions = { attributes: { test: 'value' } } as IRoktSelectPlacementsOptions; - const hashAttrs = { email: 'test@example.com' }; const extensionData = { 'test-ext': { config: true } }; 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 // Spy on RoktManager methods AFTER initial calls to track queue processing const selectPlacementsSpy = jest.spyOn(roktManager, 'selectPlacements'); - const hashAttributesSpy = jest.spyOn(roktManager, 'hashAttributes'); const setExtensionDataSpy = jest.spyOn(roktManager, 'setExtensionData'); const useSpy = jest.spyOn(roktManager, 'use'); @@ -585,9 +527,6 @@ describe('RoktManager', () => { expect(selectPlacementsSpy).toHaveBeenCalledTimes(1); expect(selectPlacementsSpy).toHaveBeenCalledWith(selectOptions); - expect(hashAttributesSpy).toHaveBeenCalledTimes(1); - expect(hashAttributesSpy).toHaveBeenCalledWith(hashAttrs); - expect(setExtensionDataSpy).toHaveBeenCalledTimes(1); expect(setExtensionDataSpy).toHaveBeenCalledWith(extensionData); @@ -599,7 +538,6 @@ describe('RoktManager', () => { // Clean up spies selectPlacementsSpy.mockRestore(); - hashAttributesSpy.mockRestore(); setExtensionDataSpy.mockRestore(); useSpy.mockRestore(); }); @@ -846,7 +784,7 @@ describe('RoktManager', () => { expect(kit.launcher.selectPlacements).toHaveBeenCalledWith(options); }); - it('should pass sandbox flag as an attribute through to kit.selectPlacements', ()=> { + it('should pass sandbox flag as an attribute through to kit.selectPlacements', () => { const kit: IRoktKit = { launcher: { selectPlacements: jest.fn(), From 4dfbc718f6b0d60ab6dfae307a0f30a5ebeea12a Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 2 Dec 2025 15:26:05 -0500 Subject: [PATCH 6/9] parallelize hashAttributes to match Rokt wSDK --- src/roktManager.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index 57a6216f..05993bfd 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -267,20 +267,30 @@ export default class RoktManager { return {}; } - const hashedAttributes: IRoktPartnerAttributes = {}; + // Get own property keys only + const keys = Object.keys(attributes); + + if (keys.length === 0) { + return {}; + } - for (const key in attributes) { + // Hash all attributes in parallel + const hashPromises = keys.map(async (key) => { const attributeValue = attributes[key]; - - hashedAttributes[key] = attributeValue; - const hashedValue = await this.hashSha256(attributeValue); - + return { key, attributeValue, hashedValue }; + }); + + const results = await Promise.all(hashPromises); + + // Build the result object + const hashedAttributes: IRoktPartnerAttributes = {}; + for (const { key, attributeValue, hashedValue } of results) { + hashedAttributes[key] = attributeValue; if (hashedValue) { hashedAttributes[`${key}sha256`] = hashedValue; } } - return hashedAttributes; } catch (error) { From ccc9f302408173965822db2cf8ddcfc179eb5aaf Mon Sep 17 00:00:00 2001 From: Jaissica Date: Wed, 3 Dec 2025 10:58:47 -0500 Subject: [PATCH 7/9] update logger error message for hashAttributes and hashSha256 --- src/roktManager.ts | 6 ++++-- test/jest/roktManager.spec.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index 05993bfd..0904dfb9 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -294,7 +294,9 @@ export default class RoktManager { return hashedAttributes; } catch (error) { - return Promise.reject(error instanceof Error ? error : new Error('Failed to hash attributes')); + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger.error(`Failed to hash "${attributes}", selectPlacements will continue: ${errorMessage}`); + return {}; } } @@ -342,7 +344,7 @@ export default class RoktManager { return await this.sha256Hex(normalizedValue); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to hash "${attribute}" and returning undefined, selectPlacements will continue: ${errorMessage}`); + this.logger.error(`Failed to hash "${attribute}" and returning undefined: ${errorMessage}`); return undefined; } } diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 6f9ef3eb..a7ad261c 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -145,7 +145,7 @@ describe('RoktManager', () => { expect(result).toEqual({}); }); - it('should reject if hashSha256 throws an error', async () => { + it('should log error if hashSha256 throws an error', async () => { shaSpy.mockRestore(); // Mock hashSha256 to throw an error @@ -153,10 +153,12 @@ describe('RoktManager', () => { jest.spyOn(roktManager, 'hashSha256').mockRejectedValue(hashError); const attributes = { email: 'test@example.com' }; + const result = await roktManager.hashAttributes(attributes); - await expect(roktManager.hashAttributes(attributes)) - .rejects - .toThrow('Hashing failed'); + expect(result).toEqual({}); + expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to hash "') + ); }); }); @@ -222,7 +224,7 @@ describe('RoktManager', () => { expect(result).toBeUndefined(); expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( - expect.stringContaining('Failed to hash "test@example.com" and returning undefined, selectPlacements will continue') + expect.stringContaining('Failed to hash "test@example.com" and returning undefined') ); }); From fa03a208a68fb7979e1f2a97a442d624f09e9d5e Mon Sep 17 00:00:00 2001 From: Jaissica Date: Wed, 3 Dec 2025 12:16:47 -0500 Subject: [PATCH 8/9] update logger warning and error message to exclude user attributes --- src/roktManager.ts | 6 +++--- test/jest/roktManager.spec.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index 0904dfb9..051ff819 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -295,7 +295,7 @@ export default class RoktManager { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to hash "${attributes}", selectPlacements will continue: ${errorMessage}`); + this.logger.error(`Failed to hashAttributes, returning an empty object: ${errorMessage}`); return {}; } } @@ -335,7 +335,7 @@ export default class RoktManager { */ public async hashSha256(attribute: string | number | boolean | undefined | null): Promise { if (attribute === null || attribute === undefined) { - this.logger.warning(`hashSha256 received ${attribute} as input`); + this.logger.warning(`hashSha256 received null/undefined as input`); return attribute as null | undefined; } @@ -344,7 +344,7 @@ export default class RoktManager { return await this.sha256Hex(normalizedValue); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to hash "${attribute}" and returning undefined: ${errorMessage}`); + this.logger.error(`Failed to hashSha256 and returning undefined: ${errorMessage}`); return undefined; } } diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index a7ad261c..6b907fbe 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -157,7 +157,7 @@ describe('RoktManager', () => { expect(result).toEqual({}); expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( - expect.stringContaining('Failed to hash "') + expect.stringContaining('Failed to hashAttributes, returning an empty object: Hashing failed') ); }); }); @@ -207,14 +207,14 @@ describe('RoktManager', () => { const result = await roktManager.hashSha256(null); expect(result).toBeNull(); - expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith('hashSha256 received null as input'); + expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith('hashSha256 received null/undefined as input'); }); it('should return undefined and log warning when value is undefined', async () => { const result = await roktManager.hashSha256(undefined); expect(result).toBeUndefined(); - expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith('hashSha256 received undefined as input'); + expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith('hashSha256 received null/undefined as input'); }); it('should return undefined and log error when hashing fails', async () => { @@ -224,7 +224,7 @@ describe('RoktManager', () => { expect(result).toBeUndefined(); expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( - expect.stringContaining('Failed to hash "test@example.com" and returning undefined') + expect.stringContaining('Failed to hashSha256 and returning undefined: Hash failed') ); }); From 293e290b459932c2ec6185ac06a1a02f764832ed Mon Sep 17 00:00:00 2001 From: Jaissica Date: Wed, 3 Dec 2025 12:22:11 -0500 Subject: [PATCH 9/9] remove and from hashSha256 error message and update related test --- src/roktManager.ts | 2 +- test/jest/roktManager.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index 051ff819..c04aefa5 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -344,7 +344,7 @@ export default class RoktManager { return await this.sha256Hex(normalizedValue); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - this.logger.error(`Failed to hashSha256 and returning undefined: ${errorMessage}`); + this.logger.error(`Failed to hashSha256, returning undefined: ${errorMessage}`); return undefined; } } diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 6b907fbe..cb901838 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -224,7 +224,7 @@ describe('RoktManager', () => { expect(result).toBeUndefined(); expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( - expect.stringContaining('Failed to hashSha256 and returning undefined: Hash failed') + expect.stringContaining('Failed to hashSha256, returning undefined: Hash failed') ); });