diff --git a/package.json b/package.json index 6321cca..2bc9094 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,7 @@ "format": "npx prettier --check 'src/**/*.{js,ts}'", "lint": "npx eslint src", "test": "npm run prebuild && npx jest -w 1 --coverage", - "clean": "rimraf lib-esm lib dist", - "pack": "npm run build && npm pack", - "publish": "npm run pack && npm publish" + "clean": "rimraf lib-esm lib dist" }, "repository": { "type": "git", diff --git a/src/browser/BrowserInfo.ts b/src/browser/BrowserInfo.ts index fbc0792..f4f9ead 100644 --- a/src/browser/BrowserInfo.ts +++ b/src/browser/BrowserInfo.ts @@ -1,4 +1,5 @@ import { Logger } from '@aws-amplify/core'; +import { StorageUtil } from '../util/StorageUtil'; /** * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -84,7 +85,7 @@ export class BrowserInfo { const performanceEntries = performance.getEntriesByType('navigation'); if (performanceEntries && performanceEntries.length > 0) { const type = (performanceEntries[0] as any)['type']; - return type === 'reload'; + return type === 'reload' && StorageUtil.getPreviousPageUrl() !== ''; } } else { logger.warn('unsupported web environment for performance'); diff --git a/src/tracker/PageViewTracker.ts b/src/tracker/PageViewTracker.ts index c506b72..bab38de 100644 --- a/src/tracker/PageViewTracker.ts +++ b/src/tracker/PageViewTracker.ts @@ -25,17 +25,20 @@ export class PageViewTracker extends BaseTracker { searchKeywords = Event.Constants.KEYWORDS; lastEngageTime = 0; lastScreenStartTimestamp = 0; + isFirstTime = true; init() { const configuredSearchKeywords = this.provider.configuration.searchKeyWords; Object.assign(this.searchKeywords, configuredSearchKeywords); this.onPageChange = this.onPageChange.bind(this); - if (this.context.configuration.pageType === PageType.SPA) { - this.trackPageViewForSPA(); - } else { + if (this.isMultiPageApp()) { if (!BrowserInfo.isFromReload()) { this.onPageChange(); + } else { + this.isFirstTime = false; } + } else { + this.trackPageViewForSPA(); } } @@ -45,6 +48,8 @@ export class PageViewTracker extends BaseTracker { window.addEventListener('popstate', this.onPageChange); if (!BrowserInfo.isFromReload()) { this.onPageChange(); + } else { + this.isFirstTime = false; } } @@ -55,11 +60,17 @@ export class PageViewTracker extends BaseTracker { const currentPageUrl = BrowserInfo.getCurrentPageUrl(); const currentPageTitle = BrowserInfo.getCurrentPageTitle(); if ( + this.isFirstTime || + this.isMultiPageApp() || previousPageUrl !== currentPageUrl || previousPageTitle !== currentPageTitle ) { this.provider.scrollTracker?.enterNewPage(); - if (previousPageUrl !== '') { + if ( + !this.isMultiPageApp() && + !this.isFirstTime && + previousPageUrl !== '' + ) { this.recordUserEngagement(); } this.trackPageView(previousPageUrl, previousPageTitle); @@ -67,6 +78,9 @@ export class PageViewTracker extends BaseTracker { StorageUtil.savePreviousPageUrl(currentPageUrl); StorageUtil.savePreviousPageTitle(currentPageTitle); + if (this.isFirstTime) { + this.isFirstTime = false; + } } } } @@ -128,6 +142,10 @@ export class PageViewTracker extends BaseTracker { return new Date().getTime() - this.lastScreenStartTimestamp; } + isMultiPageApp() { + return this.context.configuration.pageType === PageType.multiPageApp; + } + trackSearchEvents() { if (!this.context.configuration.isTrackSearchEvents) return; const searchStr = window.location.search; diff --git a/src/tracker/SessionTracker.ts b/src/tracker/SessionTracker.ts index ce42122..9ac6801 100644 --- a/src/tracker/SessionTracker.ts +++ b/src/tracker/SessionTracker.ts @@ -52,7 +52,6 @@ export class SessionTracker extends BaseTracker { handleInit() { this.session = Session.getCurrentSession(this.context); - StorageUtil.clearPageInfo(); if (StorageUtil.getIsFirstOpen()) { this.provider.record({ name: Event.PresetEvent.FIRST_OPEN, @@ -69,7 +68,11 @@ export class SessionTracker extends BaseTracker { this.session = Session.getCurrentSession(this.context); if (this.session.isNewSession()) { pageViewTracker.setIsEntrances(); + StorageUtil.clearPageInfo(); this.provider.record({ name: Event.PresetEvent.SESSION_START }); + if(!isFirstTime){ + pageViewTracker.onPageChange(); + } } if (!this.provider.configuration.isTrackAppStartEvents) return; if (isFirstTime && this.isFromCurrentHost()) return; @@ -89,6 +92,7 @@ export class SessionTracker extends BaseTracker { onPageHide() { logger.debug('page hide'); this.storeSession(); + StorageUtil.checkClickstreamId(); const isImmediate = !(this.isWindowClosing && BrowserInfo.isFirefox()); this.recordUserEngagement(isImmediate); this.recordAppEnd(isImmediate); diff --git a/src/util/StorageUtil.ts b/src/util/StorageUtil.ts index 6c9134b..df2d03a 100644 --- a/src/util/StorageUtil.ts +++ b/src/util/StorageUtil.ts @@ -39,28 +39,39 @@ export class StorageUtil { static readonly previousPageStartTimeKey = this.prefix + 'previousPageStartTimeKey'; static readonly userIdMappingKey = this.prefix + 'userIdMappingKey'; + private static deviceId = ''; + private static userUniqueId = ''; static getDeviceId(): string { - let deviceId = localStorage.getItem(StorageUtil.deviceIdKey) ?? ''; - if (deviceId === '') { + if (StorageUtil.deviceId !== '') { + return StorageUtil.deviceId; + } + let deviceId = localStorage.getItem(StorageUtil.deviceIdKey); + if (deviceId === null) { deviceId = uuidV4(); localStorage.setItem(StorageUtil.deviceIdKey, deviceId); } + StorageUtil.deviceId = deviceId; return deviceId; } static setCurrentUserUniqueId(userUniqueId: string) { + StorageUtil.userUniqueId = userUniqueId; localStorage.setItem(StorageUtil.userUniqueIdKey, userUniqueId); } static getCurrentUserUniqueId(): string { - let userUniqueId = localStorage.getItem(StorageUtil.userUniqueIdKey) ?? ''; - if (userUniqueId === '') { + if (StorageUtil.userUniqueId !== '') { + return StorageUtil.userUniqueId; + } + let userUniqueId = localStorage.getItem(StorageUtil.userUniqueIdKey); + if (userUniqueId === null) { userUniqueId = uuidV4(); StorageUtil.setCurrentUserUniqueId(userUniqueId); localStorage.setItem(StorageUtil.userUniqueIdKey, userUniqueId); StorageUtil.saveUserFirstTouchTimestamp(); } + StorageUtil.userUniqueId = userUniqueId; return userUniqueId; } @@ -293,4 +304,40 @@ export class StorageUtil { timestamp.toString() ); } + + static checkDeviceId() { + const currentDeviceId = localStorage.getItem(StorageUtil.deviceIdKey) ?? ''; + if (StorageUtil.deviceId !== '' && currentDeviceId === '') { + localStorage.setItem(StorageUtil.deviceIdKey, StorageUtil.deviceId); + } + } + + static checkUserUniqueId() { + const currentUserUniqueId = + localStorage.getItem(StorageUtil.userUniqueIdKey) ?? ''; + if (StorageUtil.userUniqueId !== '' && currentUserUniqueId === '') { + localStorage.setItem( + StorageUtil.userUniqueIdKey, + StorageUtil.userUniqueId + ); + } + } + + static checkIsFirstOpen() { + if (StorageUtil.getIsFirstOpen()) { + StorageUtil.saveIsFirstOpenToFalse(); + } + } + + static checkClickstreamId() { + StorageUtil.checkDeviceId(); + StorageUtil.checkUserUniqueId(); + StorageUtil.checkIsFirstOpen(); + } + + static clearAll() { + localStorage.clear(); + (StorageUtil as any).deviceid = ''; + (StorageUtil as any).userUniqueId = ''; + } } diff --git a/test/ClickstreamAnalytics.test.ts b/test/ClickstreamAnalytics.test.ts index 6706bd9..22685d5 100644 --- a/test/ClickstreamAnalytics.test.ts +++ b/test/ClickstreamAnalytics.test.ts @@ -18,7 +18,7 @@ import { StorageUtil } from '../src/util/StorageUtil'; describe('ClickstreamAnalytics test', () => { beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll(); const mockSendRequestSuccess = jest.fn().mockResolvedValue(true); jest .spyOn(NetRequest, 'sendRequest') diff --git a/test/browser/BrowserInfo.test.ts b/test/browser/BrowserInfo.test.ts index dacb91b..fe26550 100644 --- a/test/browser/BrowserInfo.test.ts +++ b/test/browser/BrowserInfo.test.ts @@ -13,6 +13,7 @@ import { setPerformanceEntries } from './BrowserUtil'; import { MockObserver } from './MockObserver'; import { BrowserInfo } from '../../src/browser'; +import { StorageUtil } from '../../src/util/StorageUtil'; describe('BrowserInfo test', () => { afterEach(() => { @@ -20,6 +21,7 @@ describe('BrowserInfo test', () => { jest.resetAllMocks(); }); test('test create BrowserInfo', () => { + StorageUtil.clearAllEvents() const referrer = 'https://example.com/collect'; Object.defineProperty(window.document, 'referrer', { writable: true, @@ -109,6 +111,7 @@ describe('BrowserInfo test', () => { test('test web page not from reload', () => { (global as any).PerformanceObserver = MockObserver; setPerformanceEntries(true, true); + StorageUtil.savePreviousPageUrl("http://localhost:8080") expect(BrowserInfo.isFromReload()).toBeTruthy(); }); }); diff --git a/test/provider/BatchModeTimer.test.ts b/test/provider/BatchModeTimer.test.ts index a4005c1..037e6bf 100644 --- a/test/provider/BatchModeTimer.test.ts +++ b/test/provider/BatchModeTimer.test.ts @@ -13,12 +13,13 @@ import { SendMode } from '../../src'; import { NetRequest } from '../../src/network/NetRequest'; import { ClickstreamProvider } from '../../src/provider'; +import { StorageUtil } from '../../src/util/StorageUtil'; import { setUpBrowserPerformance } from '../browser/BrowserUtil'; describe('ClickstreamProvider timer test', () => { let provider: ClickstreamProvider; beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll(); setUpBrowserPerformance(); provider = new ClickstreamProvider(); const mockSendRequest = jest.fn().mockResolvedValue(true); diff --git a/test/provider/ClickstreamProvider.test.ts b/test/provider/ClickstreamProvider.test.ts index 2090b68..4e6fa3d 100644 --- a/test/provider/ClickstreamProvider.test.ts +++ b/test/provider/ClickstreamProvider.test.ts @@ -32,7 +32,7 @@ describe('ClickstreamProvider test', () => { let mockRecordProfileSet: any; beforeEach(async () => { - localStorage.clear(); + StorageUtil.clearAll(); setUpBrowserPerformance(); const mockSendRequest = jest.fn().mockResolvedValue(true); jest.spyOn(NetRequest, 'sendRequest').mockImplementation(mockSendRequest); diff --git a/test/provider/EventRecorder.test.ts b/test/provider/EventRecorder.test.ts index bbc60f8..d1ac676 100644 --- a/test/provider/EventRecorder.test.ts +++ b/test/provider/EventRecorder.test.ts @@ -26,7 +26,7 @@ describe('EventRecorder test', () => { let eventRecorder: EventRecorder; let context: ClickstreamContext; beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll() context = new ClickstreamContext(new BrowserInfo(), { appId: 'testApp', endpoint: 'https://localhost:8080/collect', diff --git a/test/tracker/ClickTracker.test.ts b/test/tracker/ClickTracker.test.ts index f67c25f..da492c6 100644 --- a/test/tracker/ClickTracker.test.ts +++ b/test/tracker/ClickTracker.test.ts @@ -20,6 +20,7 @@ import { } from '../../src/provider'; import { Session, SessionTracker } from '../../src/tracker'; import { ClickTracker } from '../../src/tracker/ClickTracker'; +import { StorageUtil } from "../../src/util/StorageUtil"; describe('ClickTracker test', () => { let provider: ClickstreamProvider; @@ -28,7 +29,7 @@ describe('ClickTracker test', () => { let recordMethodMock: any; beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll() provider = new ClickstreamProvider(); Object.assign(provider.configuration, { appId: 'testAppId', diff --git a/test/tracker/PageLoadTracker.test.ts b/test/tracker/PageLoadTracker.test.ts index 92212eb..a78c36d 100644 --- a/test/tracker/PageLoadTracker.test.ts +++ b/test/tracker/PageLoadTracker.test.ts @@ -21,6 +21,7 @@ import { Session, SessionTracker } from '../../src/tracker'; import { PageLoadTracker } from '../../src/tracker/PageLoadTracker'; import { setPerformanceEntries } from '../browser/BrowserUtil'; import { MockObserver } from '../browser/MockObserver'; +import { StorageUtil } from "../../src/util/StorageUtil"; describe('PageLoadTracker test', () => { let provider: ClickstreamProvider; @@ -29,7 +30,7 @@ describe('PageLoadTracker test', () => { let recordMethodMock: any; beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll() provider = new ClickstreamProvider(); Object.assign(provider.configuration, { appId: 'testAppId', diff --git a/test/tracker/PageViewTracker.test.ts b/test/tracker/PageViewTracker.test.ts index 8e4fe27..5c42c1c 100644 --- a/test/tracker/PageViewTracker.test.ts +++ b/test/tracker/PageViewTracker.test.ts @@ -28,8 +28,8 @@ import { import { PageViewTracker, Session, SessionTracker } from '../../src/tracker'; import { MethodEmbed } from '../../src/util/MethodEmbed'; import { StorageUtil } from '../../src/util/StorageUtil'; -import { setPerformanceEntries } from "../browser/BrowserUtil"; -import { MockObserver } from "../browser/MockObserver"; +import { setPerformanceEntries } from '../browser/BrowserUtil'; +import { MockObserver } from '../browser/MockObserver'; global.TextEncoder = TextEncoder; global.TextDecoder = TextDecoder; @@ -46,7 +46,7 @@ describe('PageViewTracker test', () => { let dom: any; beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll(); provider = new ClickstreamProvider(); Object.assign(provider.configuration, { @@ -76,7 +76,7 @@ describe('PageViewTracker test', () => { value: 'index', }); (global as any).PerformanceObserver = MockObserver; - setPerformanceEntries() + setPerformanceEntries(); }); afterEach(() => { @@ -95,6 +95,33 @@ describe('PageViewTracker test', () => { expect(pageAppearMock).toBeCalled(); }); + test('test multiPageApp do not record page view when browser reload', () => { + const pageAppearMock = jest.spyOn(pageViewTracker, 'onPageChange'); + pageViewTracker.setUp(); + expect(pageAppearMock).toBeCalled(); + (global as any).PerformanceObserver = MockObserver; + setPerformanceEntries(true, true); + pageViewTracker.setUp(); + expect(pageAppearMock).toBeCalledTimes(1); + }); + + test('test isFirstTime to be false when browser reload in SPA mode', () => { + (global as any).PerformanceObserver = MockObserver; + StorageUtil.savePreviousPageUrl('https://example.com/pageA'); + setPerformanceEntries(true, true); + pageViewTracker.setUp(); + expect(pageViewTracker.isFirstTime).toBeFalsy(); + }); + + test('test isFirstTime to be false when browser reload in multiPageApp mode', () => { + (context.configuration as any).pageType = PageType.multiPageApp; + StorageUtil.savePreviousPageUrl('https://example.com/pageA'); + (global as any).PerformanceObserver = MockObserver; + setPerformanceEntries(true, true); + pageViewTracker.setUp(); + expect(pageViewTracker.isFirstTime).toBeFalsy(); + }); + test('test environment is not supported', () => { const addEventListener = (global as any).window.addEventListener; (global as any).window.addEventListener = undefined; @@ -120,6 +147,22 @@ describe('PageViewTracker test', () => { expect(pageAppearMock).toBeCalledTimes(1); }); + test('reload browser will not record page view in SPA mode', async () => { + (context.configuration as any).pageType = PageType.SPA; + const pageAppearMock = jest.spyOn(pageViewTracker, 'onPageChange'); + pageViewTracker.setUp(); + expect(recordEventMethodMock).toBeCalledWith( + expect.objectContaining({ + event_type: Event.PresetEvent.PAGE_VIEW, + }) + ); + expect(pageAppearMock).toBeCalledTimes(1); + (global as any).PerformanceObserver = MockObserver; + setPerformanceEntries(true, true); + pageViewTracker.setUp(); + expect(pageAppearMock).toBeCalledTimes(1); + }); + test('test two different page view with userEngagement', async () => { pageViewTracker.setUp(); jest.spyOn(pageViewTracker, 'getLastEngageTime').mockReturnValue(1100); diff --git a/test/tracker/ScrollTracker.test.ts b/test/tracker/ScrollTracker.test.ts index 678d7e2..ad1da47 100644 --- a/test/tracker/ScrollTracker.test.ts +++ b/test/tracker/ScrollTracker.test.ts @@ -19,6 +19,7 @@ import { } from '../../src/provider'; import { Session, SessionTracker } from '../../src/tracker'; import { ScrollTracker } from '../../src/tracker/ScrollTracker'; +import { StorageUtil } from "../../src/util/StorageUtil"; describe('ScrollTracker test', () => { let provider: ClickstreamProvider; @@ -27,7 +28,7 @@ describe('ScrollTracker test', () => { let recordMethodMock: any; beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll() provider = new ClickstreamProvider(); Object.assign(provider.configuration, { diff --git a/test/tracker/SessionTracker.test.ts b/test/tracker/SessionTracker.test.ts index 8fbfcc1..1d550f7 100644 --- a/test/tracker/SessionTracker.test.ts +++ b/test/tracker/SessionTracker.test.ts @@ -35,12 +35,13 @@ import { MockObserver } from '../browser/MockObserver'; describe('SessionTracker test', () => { let provider: ClickstreamProvider; let sessionTracker: SessionTracker; + let pageViewTracker: PageViewTracker; let context: ClickstreamContext; let eventRecorder: EventRecorder; let recordMethodMock: any; beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll(); const mockSendRequest = jest.fn().mockResolvedValue(true); jest.spyOn(NetRequest, 'sendRequest').mockImplementation(mockSendRequest); provider = new ClickstreamProvider(); @@ -54,7 +55,7 @@ describe('SessionTracker test', () => { eventRecorder = new EventRecorder(context); sessionTracker = new SessionTracker(provider, context); - const pageViewTracker = new PageViewTracker(provider, context); + pageViewTracker = new PageViewTracker(provider, context); provider.context = context; provider.sessionTracker = sessionTracker; provider.eventRecorder = eventRecorder; @@ -150,6 +151,8 @@ describe('SessionTracker test', () => { }); test('test not record app start when browser is from reload', () => { + provider.configuration.isTrackAppStartEvents = true; + jest.spyOn(BrowserInfo, 'isFromReload').mockReturnValue(true); setPerformanceEntries(true, true); sessionTracker.setUp(); expect(recordMethodMock).not.toBeCalledWith({ @@ -208,13 +211,28 @@ describe('SessionTracker test', () => { expect(sessionTracker.session.sessionIndex).toBe(1); }); - test('test session timeout', async () => { + test('test hide page when localStorage cleared', () => { + const onPageHideMock = jest.spyOn(sessionTracker, 'onPageHide'); + sessionTracker.setUp(); + const originDeviceId = StorageUtil.getDeviceId(); + const originUserUniqueId = StorageUtil.getCurrentUserUniqueId(); + localStorage.clear(); + hidePage(); + expect(onPageHideMock).toBeCalled(); + expect(StorageUtil.getDeviceId()).toBe(originDeviceId); + expect(StorageUtil.getCurrentUserUniqueId()).toBe(originUserUniqueId); + expect(StorageUtil.getIsFirstOpen()).toBe(false); + }); + + test('test session timeout and reopen the page will record page view', async () => { + const trackPageViewMock = jest.spyOn(pageViewTracker, 'trackPageView'); (provider.configuration as any).sessionTimeoutDuration = 0; sessionTracker.setUp(); hidePage(); await sleep(100); showPage(); expect(sessionTracker.session.sessionIndex).toBe(2); + expect(trackPageViewMock).toBeCalled(); }); test('test send event in batch mode when hide page', async () => { @@ -280,8 +298,8 @@ describe('SessionTracker test', () => { hidePage(); expect(sendEventBackgroundMock).toBeCalledWith(false); expect(flushEventMock).toBeCalled(); - expect(clearAllEventsMock).not.toBeCalled(); expect(recordUserEngagementMock).toBeCalledWith(true); + expect(clearAllEventsMock).not.toBeCalled(); }); test('test send event in batch mode when close window in firefox', async () => { diff --git a/test/util/StorageUtil.test.ts b/test/util/StorageUtil.test.ts index c83cb6c..891e760 100644 --- a/test/util/StorageUtil.test.ts +++ b/test/util/StorageUtil.test.ts @@ -22,7 +22,7 @@ import { StorageUtil } from '../../src/util/StorageUtil'; describe('StorageUtil test', () => { beforeEach(() => { - localStorage.clear(); + StorageUtil.clearAll() }); test('test get device id', () => {