From fae5c8ca87405f398e3999c2672848c533e77b5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:32:16 +0000 Subject: [PATCH 1/5] Initial plan From d075c926b7079fafe908aa4bd2409d866baa929c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:39:27 +0000 Subject: [PATCH 2/5] Wrap localStorage access in try-catch blocks to handle SecurityError --- .../src/tracker/local_storage_event_store.ts | 16 +- .../tracker/local_storage_event_store.test.ts | 181 ++++++++++++++++++ 2 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts diff --git a/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts b/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts index 52f7db9aa..cea5bc226 100644 --- a/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts +++ b/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts @@ -21,9 +21,13 @@ export function newLocalStorageEventStore({ function newInMemoryEventStoreFromLocalStorage() { if (useLocalStorage) { - const localStorageQueue = window.localStorage.getItem(queueName); - const events: EventStorePayload[] = localStorageQueue ? JSON.parse(localStorageQueue) : []; - return newInMemoryEventStore({ maxSize: maxLocalStorageQueueSize, events }); + try { + const localStorageQueue = window.localStorage.getItem(queueName); + const events: EventStorePayload[] = localStorageQueue ? JSON.parse(localStorageQueue) : []; + return newInMemoryEventStore({ maxSize: maxLocalStorageQueueSize, events }); + } catch (e) { + return newInMemoryEventStore({ maxSize: maxLocalStorageQueueSize }); + } } else { return newInMemoryEventStore({ maxSize: maxLocalStorageQueueSize }); } @@ -34,7 +38,11 @@ export function newLocalStorageEventStore({ function sync(): Promise { if (useLocalStorage) { return getAll().then((events) => { - window.localStorage.setItem(queueName, JSON.stringify(events)); + try { + window.localStorage.setItem(queueName, JSON.stringify(events)); + } catch (e) { + // Silently fail if localStorage is not accessible + } }); } else { return Promise.resolve(); diff --git a/libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts b/libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts new file mode 100644 index 000000000..58a16ee38 --- /dev/null +++ b/libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import { newLocalStorageEventStore } from '../../src/tracker/local_storage_event_store'; + +describe('LocalStorageEventStore', () => { + const trackerId = 'test-tracker'; + + beforeEach(() => { + localStorage.clear(); + }); + + it('should create an event store with useLocalStorage enabled', async () => { + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: true, + }); + + expect(await eventStore.count()).toBe(0); + }); + + it('should create an event store with useLocalStorage disabled', async () => { + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: false, + }); + + expect(await eventStore.count()).toBe(0); + }); + + it('should add and retrieve events when localStorage is accessible', async () => { + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: true, + }); + + const event = { e: 'pv', eid: 'test-event-id' }; + await eventStore.add(event); + + expect(await eventStore.count()).toBe(1); + const events = await eventStore.getAllPayloads(); + expect(events[0]).toMatchObject(event); + }); + + it('should handle SecurityError when accessing localStorage.getItem', () => { + const originalGetItem = Storage.prototype.getItem; + Storage.prototype.getItem = jest.fn(() => { + throw new DOMException('The operation is insecure.', 'SecurityError'); + }); + + // Should not throw an error, but should create an empty in-memory store + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: true, + }); + + expect(eventStore).toBeDefined(); + expect(eventStore.count).toBeDefined(); + + Storage.prototype.getItem = originalGetItem; + }); + + it('should handle SecurityError when accessing localStorage.setItem', async () => { + const originalSetItem = Storage.prototype.setItem; + Storage.prototype.setItem = jest.fn(() => { + throw new DOMException('The operation is insecure.', 'SecurityError'); + }); + + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: true, + }); + + const event = { e: 'pv', eid: 'test-event-id' }; + + // Should not throw an error, even though setItem fails + await expect(eventStore.add(event)).resolves.toBeDefined(); + + // Event should still be in the in-memory store + expect(await eventStore.count()).toBe(1); + + Storage.prototype.setItem = originalSetItem; + }); + + it('should gracefully handle errors when both getItem and setItem throw SecurityError', async () => { + const originalGetItem = Storage.prototype.getItem; + const originalSetItem = Storage.prototype.setItem; + + Storage.prototype.getItem = jest.fn(() => { + throw new DOMException('The operation is insecure.', 'SecurityError'); + }); + Storage.prototype.setItem = jest.fn(() => { + throw new DOMException('The operation is insecure.', 'SecurityError'); + }); + + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: true, + }); + + const event = { e: 'pv', eid: 'test-event-id' }; + await eventStore.add(event); + + // Event should be in the in-memory store + expect(await eventStore.count()).toBe(1); + const events = await eventStore.getAllPayloads(); + expect(events[0]).toMatchObject(event); + + Storage.prototype.getItem = originalGetItem; + Storage.prototype.setItem = originalSetItem; + }); + + it('should persist events to localStorage when accessible', async () => { + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: true, + }); + + const event = { e: 'pv', eid: 'test-event-id' }; + await eventStore.add(event); + + // Check that the event was persisted to localStorage + const queueName = `snowplowOutQueue_${trackerId}`; + const stored = localStorage.getItem(queueName); + expect(stored).toBeDefined(); + expect(JSON.parse(stored!)).toHaveLength(1); + }); + + it('should load events from localStorage on initialization', () => { + const queueName = `snowplowOutQueue_${trackerId}`; + const events = [{ e: 'pv', eid: 'event-1' }, { e: 'pv', eid: 'event-2' }]; + localStorage.setItem(queueName, JSON.stringify(events)); + + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: true, + }); + + expect(eventStore.count()).resolves.toBe(2); + }); + + it('should not load from localStorage when useLocalStorage is false', () => { + const queueName = `snowplowOutQueue_${trackerId}`; + const events = [{ e: 'pv', eid: 'event-1' }, { e: 'pv', eid: 'event-2' }]; + localStorage.setItem(queueName, JSON.stringify(events)); + + const eventStore = newLocalStorageEventStore({ + trackerId, + useLocalStorage: false, + }); + + expect(eventStore.count()).resolves.toBe(0); + }); +}); From d04777608056d9c05859b7374ca4df59f34e7338 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:09:15 +0000 Subject: [PATCH 3/5] Add console.error logging for localStorage access failures Co-authored-by: matus-tomlein <42166+matus-tomlein@users.noreply.github.com> --- .../src/tracker/local_storage_event_store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts b/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts index cea5bc226..b6e554aa4 100644 --- a/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts +++ b/libraries/browser-tracker-core/src/tracker/local_storage_event_store.ts @@ -26,6 +26,7 @@ export function newLocalStorageEventStore({ const events: EventStorePayload[] = localStorageQueue ? JSON.parse(localStorageQueue) : []; return newInMemoryEventStore({ maxSize: maxLocalStorageQueueSize, events }); } catch (e) { + console.error('Failed to access localStorage when initializing event store:', e); return newInMemoryEventStore({ maxSize: maxLocalStorageQueueSize }); } } else { @@ -41,7 +42,7 @@ export function newLocalStorageEventStore({ try { window.localStorage.setItem(queueName, JSON.stringify(events)); } catch (e) { - // Silently fail if localStorage is not accessible + console.error('Failed to persist events to localStorage:', e); } }); } else { From 66c256f0059bb4b0a4a5cc3ce512d8c12910389f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:23:43 +0000 Subject: [PATCH 4/5] Fix test to use proper EventStorePayload format Co-authored-by: matus-tomlein <42166+matus-tomlein@users.noreply.github.com> --- .../tracker/local_storage_event_store.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts b/libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts index 58a16ee38..2c1ceebcf 100644 --- a/libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts +++ b/libraries/browser-tracker-core/test/tracker/local_storage_event_store.test.ts @@ -61,12 +61,12 @@ describe('LocalStorageEventStore', () => { useLocalStorage: true, }); - const event = { e: 'pv', eid: 'test-event-id' }; + const event = { payload: { e: 'pv', eid: 'test-event-id' } }; await eventStore.add(event); expect(await eventStore.count()).toBe(1); const events = await eventStore.getAllPayloads(); - expect(events[0]).toMatchObject(event); + expect(events[0]).toMatchObject(event.payload); }); it('should handle SecurityError when accessing localStorage.getItem', () => { @@ -98,7 +98,7 @@ describe('LocalStorageEventStore', () => { useLocalStorage: true, }); - const event = { e: 'pv', eid: 'test-event-id' }; + const event = { payload: { e: 'pv', eid: 'test-event-id' } }; // Should not throw an error, even though setItem fails await expect(eventStore.add(event)).resolves.toBeDefined(); @@ -125,13 +125,13 @@ describe('LocalStorageEventStore', () => { useLocalStorage: true, }); - const event = { e: 'pv', eid: 'test-event-id' }; + const event = { payload: { e: 'pv', eid: 'test-event-id' } }; await eventStore.add(event); // Event should be in the in-memory store expect(await eventStore.count()).toBe(1); const events = await eventStore.getAllPayloads(); - expect(events[0]).toMatchObject(event); + expect(events[0]).toMatchObject(event.payload); Storage.prototype.getItem = originalGetItem; Storage.prototype.setItem = originalSetItem; @@ -143,7 +143,7 @@ describe('LocalStorageEventStore', () => { useLocalStorage: true, }); - const event = { e: 'pv', eid: 'test-event-id' }; + const event = { payload: { e: 'pv', eid: 'test-event-id' } }; await eventStore.add(event); // Check that the event was persisted to localStorage @@ -155,7 +155,7 @@ describe('LocalStorageEventStore', () => { it('should load events from localStorage on initialization', () => { const queueName = `snowplowOutQueue_${trackerId}`; - const events = [{ e: 'pv', eid: 'event-1' }, { e: 'pv', eid: 'event-2' }]; + const events = [{ payload: { e: 'pv', eid: 'event-1' } }, { payload: { e: 'pv', eid: 'event-2' } }]; localStorage.setItem(queueName, JSON.stringify(events)); const eventStore = newLocalStorageEventStore({ @@ -168,7 +168,7 @@ describe('LocalStorageEventStore', () => { it('should not load from localStorage when useLocalStorage is false', () => { const queueName = `snowplowOutQueue_${trackerId}`; - const events = [{ e: 'pv', eid: 'event-1' }, { e: 'pv', eid: 'event-2' }]; + const events = [{ payload: { e: 'pv', eid: 'event-1' } }, { payload: { e: 'pv', eid: 'event-2' } }]; localStorage.setItem(queueName, JSON.stringify(events)); const eventStore = newLocalStorageEventStore({ From af2c8fb54d9419421e93681b14471aadb0f585c8 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Tue, 7 Oct 2025 15:27:29 +0200 Subject: [PATCH 5/5] Run rush change --- ...fix-localstorage-access-issue_2025-10-07-13-27.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@snowplow/browser-tracker-core/copilot-fix-localstorage-access-issue_2025-10-07-13-27.json diff --git a/common/changes/@snowplow/browser-tracker-core/copilot-fix-localstorage-access-issue_2025-10-07-13-27.json b/common/changes/@snowplow/browser-tracker-core/copilot-fix-localstorage-access-issue_2025-10-07-13-27.json new file mode 100644 index 000000000..c77bff400 --- /dev/null +++ b/common/changes/@snowplow/browser-tracker-core/copilot-fix-localstorage-access-issue_2025-10-07-13-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@snowplow/browser-tracker-core", + "comment": "Fix SecurityError when accessing localStorage in restricted browser environments", + "type": "none" + } + ], + "packageName": "@snowplow/browser-tracker-core" +} \ No newline at end of file