refactor: cookie transaction helper method#1569
Merged
daniel-graham-amplitude merged 33 commits intomainfrom Mar 5, 2026
Merged
refactor: cookie transaction helper method#1569daniel-graham-amplitude merged 33 commits intomainfrom
daniel-graham-amplitude merged 33 commits intomainfrom
Conversation
d68376f to
bc237af
Compare
1 task
Collaborator
Author
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Callback parameter shadows outer
storagevariable- Renamed the transaction callback parameter to
storageSyncso it no longer shadows the outerCookieStorageinstance.
- Renamed the transaction callback parameter to
- ✅ Fixed: Static
isDomainWritablemethod has no production callers- Updated
getTopLevelDomainin browser config to callCookieStorage.isDomainWritable, making the method used in production code.
- Updated
- ✅ Fixed: Fixed cookie key risks false positives in concurrent use
- Changed
isDomainWritableto use a UUID-suffixed cookie key per invocation, preventing cross-call cookie key collisions and false positives.
- Changed
Or push these changes by commenting:
@cursor push 4e11085af3
Preview (4e11085af3)
diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts
--- a/packages/analytics-browser/src/config.ts
+++ b/packages/analytics-browser/src/config.ts
@@ -496,23 +496,13 @@
const host = url ?? location.hostname;
const parts = host.split('.');
const levels = [];
- const cookieKeyUniqueId = UUID();
- const storageKey = `AMP_TLDTEST_${cookieKeyUniqueId.substring(0, 8)}`;
for (let i = parts.length - 2; i >= 0; --i) {
levels.push(parts.slice(i).join('.'));
}
for (let i = 0; i < levels.length; i++) {
const domain = levels[i];
- const options = {
- domain: '.' + domain,
- expirationDays: 0.003, // expire in ~5 minutes
- };
- const storage = new CookieStorage<number>(options);
- await storage.set(storageKey, 1);
- const value = await storage.get(storageKey);
- if (value) {
- await storage.remove(storageKey);
+ if (await CookieStorage.isDomainWritable(domain)) {
return '.' + domain;
}
}
diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts
--- a/packages/analytics-browser/test/config.test.ts
+++ b/packages/analytics-browser/test/config.test.ts
@@ -4,7 +4,6 @@
import * as core from '@amplitude/analytics-core';
import {
LogLevel,
- Storage,
UserSession,
MemoryStorage,
getCookieName,
@@ -400,53 +399,23 @@
});
test('should return empty string if no access to cookies', async () => {
- const testCookieStorage: Storage<number> = {
- isEnabled: () => Promise.resolve(false),
- get: jest.fn().mockResolvedValueOnce(Promise.resolve(1)),
- getRaw: jest.fn().mockResolvedValueOnce(Promise.resolve(1)),
- set: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)),
- remove: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)),
- reset: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)),
- };
- jest.spyOn(BrowserUtils, 'CookieStorage').mockReturnValueOnce({
- ...testCookieStorage,
- options: {},
- config: {},
- } as unknown as BrowserUtils.CookieStorage<unknown>);
+ const isEnabledSpy = jest.spyOn(BrowserUtils.CookieStorage.prototype, 'isEnabled').mockResolvedValueOnce(false);
const domain = await Config.getTopLevelDomain();
+ isEnabledSpy.mockRestore();
expect(domain).toBe('');
});
test('should return top level domain', async () => {
- const testCookieStorage: Storage<number> = {
- isEnabled: () => Promise.resolve(true),
- get: jest.fn().mockResolvedValueOnce(Promise.resolve(1)),
- getRaw: jest.fn().mockResolvedValueOnce(Promise.resolve(1)),
- set: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)),
- remove: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)),
- reset: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)),
- };
- const actualCookieStorage: Storage<number> = {
- isEnabled: () => Promise.resolve(true),
- get: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)).mockResolvedValueOnce(Promise.resolve(1)),
- getRaw: jest.fn().mockResolvedValueOnce(Promise.resolve(undefined)).mockResolvedValueOnce(Promise.resolve(1)),
- set: jest.fn().mockResolvedValue(Promise.resolve(undefined)),
- remove: jest.fn().mockResolvedValue(Promise.resolve(undefined)),
- reset: jest.fn().mockResolvedValue(Promise.resolve(undefined)),
- };
- jest
- .spyOn(BrowserUtils, 'CookieStorage')
- .mockReturnValueOnce({
- ...testCookieStorage,
- options: {},
- config: {},
- } as unknown as BrowserUtils.CookieStorage<unknown>)
- .mockReturnValue({
- ...actualCookieStorage,
- options: {},
- config: {},
- } as unknown as BrowserUtils.CookieStorage<unknown>);
+ const isEnabledSpy = jest.spyOn(BrowserUtils.CookieStorage.prototype, 'isEnabled').mockResolvedValueOnce(true);
+ const isDomainWritableSpy = jest
+ .spyOn(BrowserUtils.CookieStorage, 'isDomainWritable')
+ .mockResolvedValueOnce(false)
+ .mockResolvedValueOnce(true);
expect(await Config.getTopLevelDomain('www.legislation.gov.uk')).toBe('.legislation.gov.uk');
+ expect(isDomainWritableSpy).toHaveBeenNthCalledWith(1, 'gov.uk');
+ expect(isDomainWritableSpy).toHaveBeenNthCalledWith(2, 'legislation.gov.uk');
+ isEnabledSpy.mockRestore();
+ isDomainWritableSpy.mockRestore();
});
test('should not throw an error when location is an empty object', async () => {
diff --git a/packages/analytics-core/src/storage/cookie.ts b/packages/analytics-core/src/storage/cookie.ts
--- a/packages/analytics-core/src/storage/cookie.ts
+++ b/packages/analytics-core/src/storage/cookie.ts
@@ -221,13 +221,14 @@
static async isDomainWritable(domain: string): Promise<boolean> {
const options = {
domain: '.' + domain,
+ expirationDays: 0.003, // expire in ~5 minutes
};
- const storageKey = 'AMP_TLDTEST';
+ const storageKey = `AMP_TLDTEST_${UUID().substring(0, 8)}`;
const storage = new CookieStorage<number>(options);
try {
- const res = await storage.transaction(storageKey, (storage) => {
- storage.set(1);
- return storage.get();
+ const res = await storage.transaction(storageKey, (storageSync) => {
+ storageSync.set(1);
+ return storageSync.get();
});
return !!res;
} catch (error) {There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
crleona
approved these changes
Mar 5, 2026
Mercy811
approved these changes
Mar 5, 2026
Contributor
Mercy811
left a comment
There was a problem hiding this comment.
Thanks @daniel-graham-amplitude, LGTM!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Additions to "cookie.ts"
transaction(keyName, cb)that applies a global lock onkeyNameand then runs a function callbackcb.cbis run with an arg{ get: () => {}, set: () => {} }that runs synchronous operations on the cookie withkeyName--> this is so that operations that require multiple steps can block all other instances from operating on that cookie while it does it's workChecklist
Note
Medium Risk
Touches core cookie storage behavior and adds cross-tab synchronization via
navigator.locks, which could impact persistence/identity in browsers with differing cookie/locks support; changes are mitigated by fallback paths and expanded tests.Overview
Introduces a lightweight cookie “transaction” helper in
CookieStoragethat uses the Web Locks API (when available) plus new syncget/set/getRawhelpers to make multi-step cookie updates atomic per key.Adds
CookieStorage.isDomainWritable()for safely probing whether a given domain can be written to via cookies, and updates browser e2e/test-server fixtures to also stress-test concurrentgetTopLevelDomaincalls; associated unit tests are expanded and some mocks are tightened with explicit casting. Minor refactor inuseBrowserConfigrenames the computed cookie domain variable todefaultCookieDomain(no behavior change).Written by Cursor Bugbot for commit 15d14a7. This will update automatically on new commits. Configure here.