-
Notifications
You must be signed in to change notification settings - Fork 57
fix: SDKE-846 - User Identity through selectPlacements #1146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
fix: SDKE-846 - User Identity through selectPlacements #1146
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes a race condition where email/emailsha256 identities were not properly propagated to selectPlacements calls when an asynchronous identify() call was made during SDK initialization. The fix ensures that current user identities are refreshed and automatically propagated to placement attributes when they're not explicitly provided.
Changes:
- Added logic to refresh user identities and automatically propagate email/emailsha256 from current user identities to
selectPlacementsattributes when not already present - Added comprehensive test coverage for email/emailsha256 propagation, race condition handling, and ensuring explicit attributes are not overridden
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/roktManager.ts | Added user identity refresh logic and automatic email/emailsha256 propagation to enrichedAttributes in selectPlacements method |
| test/jest/roktManager.spec.ts | Added 4 new test cases covering email propagation, emailsha256 propagation, race condition scenarios, and explicit attribute preservation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2790e95 to
0297078
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jaissica12
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small nit: suggestion that could simplify this a bit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| it('should propagate email after identify completes when identify is called during init', async () => { | ||
| const kit: Partial<IRoktKit> = { | ||
| launcher: { | ||
| selectPlacements: jest.fn(), | ||
| hashAttributes: jest.fn(), | ||
| use: jest.fn(), | ||
| }, | ||
| selectPlacements: jest.fn().mockResolvedValue({}), | ||
| hashAttributes: jest.fn(), | ||
| setExtensionData: jest.fn(), | ||
| }; | ||
|
|
||
| roktManager['placementAttributesMapping'] = []; | ||
| roktManager.kit = kit as IRoktKit; | ||
|
|
||
| let identifyCallback: (() => void) | null = null; | ||
| let identifyResolve: (() => void) | null = null; | ||
| const identifyPromise = new Promise<void>((resolve) => { | ||
| identifyResolve = resolve; | ||
| }); | ||
|
|
||
| const mockIdentity = { | ||
| getCurrentUser: jest.fn() | ||
| .mockReturnValueOnce({ | ||
| getUserIdentities: () => ({ | ||
| userIdentities: {} | ||
| }), | ||
| setUserAttributes: jest.fn() | ||
| }) | ||
| .mockReturnValueOnce({ | ||
| getUserIdentities: () => ({ | ||
| userIdentities: { | ||
| email: 'user@example.com' | ||
| } | ||
| }), | ||
| setUserAttributes: jest.fn() | ||
| }), | ||
| identify: jest.fn().mockImplementation((data, callback) => { | ||
| identifyCallback = callback; | ||
| setTimeout(() => { | ||
| if (identifyCallback) { | ||
| identifyCallback(); | ||
| } | ||
| if (identifyResolve) { | ||
| identifyResolve(); | ||
| } | ||
| }, 10); | ||
| }) | ||
| } as unknown as SDKIdentityApi; | ||
|
|
||
| roktManager['identityService'] = mockIdentity; | ||
|
|
||
| mockIdentity.identify({ | ||
| userIdentities: { | ||
| email: 'user@example.com' | ||
| } | ||
| }, () => {}); | ||
|
|
||
| const options: IRoktSelectPlacementsOptions = { | ||
| attributes: { | ||
| firstname: 'John' | ||
| } | ||
| }; | ||
|
|
||
| const selectPlacementsPromise = roktManager.selectPlacements(options); | ||
|
|
||
| await identifyPromise; | ||
| await selectPlacementsPromise; | ||
|
|
||
| expect(kit.selectPlacements).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| attributes: expect.objectContaining({ | ||
| email: 'user@example.com', | ||
| firstname: 'John' | ||
| }) | ||
| }) | ||
| ); | ||
| }); |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test 'should propagate email after identify completes when identify is called during init' doesn't accurately test the race condition scenario described in the PR. The test calls identify directly (line 1699) and then selectPlacements (line 1711), but doesn't set up the store.identityCallInFlight flag. This means the wait logic in selectPlacements (lines 193-199) is never triggered. To properly test the race condition fix, the test should set up store.identityCallInFlight to true before calling selectPlacements, similar to the test on line 1929.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
test/jest/roktManager.spec.ts
Outdated
| const startTime = Date.now(); | ||
| const selectPlacementsPromise = roktManager.selectPlacements(options); | ||
|
|
||
| // Wait for both identity to complete and selectPlacements to finish | ||
| await Promise.all([identityCompletePromise, identifyPromise, selectPlacementsPromise]); | ||
| const elapsedTime = Date.now() - startTime; | ||
|
|
||
| // Verify that selectPlacements waited for the identity call to complete | ||
| expect(elapsedTime).toBeGreaterThanOrEqual(100); | ||
| expect(elapsedTime).toBeLessThan(500); | ||
|
|
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timing assertions here are fragile and may cause test flakiness in CI environments or under load. The test expects completion between 100-500ms, but this assumes the setTimeout and polling intervals execute predictably. Consider either removing these timing assertions entirely (the functional behavior is already verified by checking the email was propagated), or using Jest's fake timers to control time explicitly.
| const startTime = Date.now(); | |
| const selectPlacementsPromise = roktManager.selectPlacements(options); | |
| // Wait for both identity to complete and selectPlacements to finish | |
| await Promise.all([identityCompletePromise, identifyPromise, selectPlacementsPromise]); | |
| const elapsedTime = Date.now() - startTime; | |
| // Verify that selectPlacements waited for the identity call to complete | |
| expect(elapsedTime).toBeGreaterThanOrEqual(100); | |
| expect(elapsedTime).toBeLessThan(500); | |
| const selectPlacementsPromise = roktManager.selectPlacements(options); | |
| // Wait for both identity to complete and selectPlacements to finish | |
| await Promise.all([identityCompletePromise, identifyPromise, selectPlacementsPromise]); |
test/jest/roktManager.spec.ts
Outdated
| const elapsedTime = Date.now() - startTime; | ||
| // Should have waited until identity call completed (allowing for polling interval variance) | ||
| // With 50ms polling interval, it should complete around 150-200ms | ||
| expect(elapsedTime).toBeGreaterThanOrEqual(100); | ||
| expect(elapsedTime).toBeLessThan(500); // Should complete well before max wait time |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the previous test, these timing assertions (lines 2002-2006) are fragile and may cause test flakiness. The test already verifies the functional behavior (that selectPlacements waited for identity completion), so the timing assertions add risk without much value. Consider using Jest's fake timers or removing these timing checks.
src/roktManager.ts
Outdated
| while (this.store?.identityCallInFlight && (Date.now() - startTime) < RoktManager.IDENTITY_CALL_MAX_WAIT_MS) { | ||
| await new Promise(resolve => setTimeout(resolve, RoktManager.IDENTITY_CALL_POLL_INTERVAL_MS)); | ||
| } |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The polling implementation uses a busy-wait pattern that could be inefficient if the store check is expensive. While the 50ms interval mitigates this, consider whether the identityCallInFlight flag update triggers any observable event that could be used instead of polling. However, if polling is the only option, the current implementation with a reasonable interval and max timeout is acceptable.
test/jest/roktManager.spec.ts
Outdated
| }); | ||
|
|
||
| it('should wait briefly when identity call is in flight', async () => { | ||
| jest.setTimeout(10000); // Increase timeout for this test |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting jest.setTimeout(10000) at the test level affects the timeout for this specific test, but this approach is deprecated in newer versions of Jest. Consider using the third parameter of the it() function instead: it('test name', async () => {...}, 10000). However, since the test includes timing assertions that wait for ~150ms, the default Jest timeout should be sufficient and this line may be unnecessary.
| jest.setTimeout(10000); // Increase timeout for this test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexs-mparticle I believe we can listen to the bot here and clean up
|


Background
A race condition prevented email/emailsha256 from being propagated to selectPlacements when:
selectPlacements only used attributes passed in the call and did not pull email/emailsha256 from current user identities, so selection calls could run without email even after identify was called.
What Has Changed
Screenshots/Video
Checklist
Additional Notes
Reference Issue (For employees only. Ignore if you are an outside contributor)