diff --git a/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts b/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts index 9e19768d5..96bf9a771 100644 --- a/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts +++ b/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts @@ -22,20 +22,32 @@ export const pageViewTrackingPlugin: CreatePageViewTrackingPlugin = (options: Op let localConfig: BrowserConfig; const { trackOn, trackHistoryChanges, eventType = defaultPageViewEvent } = options; + const getDecodeURI = (locationStr: string): string => { + let decodedLocationStr = locationStr; + try { + decodedLocationStr = decodeURI(locationStr); + } catch (e) { + /* istanbul ignore next */ + loggerProvider?.error('Malformed URI sequence: ', e); + } + + return decodedLocationStr; + }; + const createPageViewEvent = async (): Promise => { + /* istanbul ignore next */ + const locationHREF = getDecodeURI((typeof location !== 'undefined' && location.href) || ''); return { event_type: eventType, event_properties: { ...(await getCampaignParams()), '[Amplitude] Page Domain': /* istanbul ignore next */ (typeof location !== 'undefined' && location.hostname) || '', - '[Amplitude] Page Location': - /* istanbul ignore next */ (typeof location !== 'undefined' && location.href) || '', + '[Amplitude] Page Location': locationHREF, '[Amplitude] Page Path': - /* istanbul ignore next */ (typeof location !== 'undefined' && location.pathname) || '', + /* istanbul ignore next */ (typeof location !== 'undefined' && getDecodeURI(location.pathname)) || '', '[Amplitude] Page Title': /* istanbul ignore next */ (typeof document !== 'undefined' && document.title) || '', - '[Amplitude] Page URL': - /* istanbul ignore next */ (typeof location !== 'undefined' && location.href.split('?')[0]) || '', + '[Amplitude] Page URL': locationHREF.split('?')[0], }, }; }; diff --git a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts index 22f164704..7c1e0b8d1 100644 --- a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts +++ b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts @@ -197,6 +197,167 @@ describe('pageViewTrackingPlugin', () => { expect(track).toHaveBeenCalledTimes(1); }); + + test('should track dynamic page view with decoded URI location info', async () => { + mockConfig.pageCounter = 0; + + const amplitude = createInstance(); + const track = jest.spyOn(amplitude, 'track').mockReturnValue({ + promise: Promise.resolve({ + code: 200, + message: '', + event: { + event_type: '[Amplitude] Page Viewed', + }, + }), + }); + + const oldURL = new URL('https://www.example.com'); + mockWindowLocationFromURL(oldURL); + const plugin = pageViewTrackingPlugin(); + await plugin.setup?.(mockConfig, amplitude); + + // https://www.example.com/home-шеллы?x=test + const newURL = new URL('https://www.example.com/home-%D1%88%D0%B5%D0%BB%D0%BB%D1%8B?x=test'); + mockWindowLocationFromURL(newURL); + window.history.pushState(undefined, newURL.href); + + // Page view tracking on push state executes async + // Block event loop for 1s before asserting + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(track).toHaveBeenNthCalledWith(1, { + event_properties: { + '[Amplitude] Page Domain': oldURL.hostname, + '[Amplitude] Page Location': oldURL.toString(), + '[Amplitude] Page Path': oldURL.pathname, + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': oldURL.toString(), + }, + event_type: '[Amplitude] Page Viewed', + }); + + expect(track).toHaveBeenNthCalledWith(2, { + event_properties: { + '[Amplitude] Page Domain': newURL.hostname, + '[Amplitude] Page Location': 'https://www.example.com/home-шеллы?x=test', + '[Amplitude] Page Path': '/home-шеллы', + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': 'https://www.example.com/home-шеллы', + }, + event_type: '[Amplitude] Page Viewed', + }); + + expect(track).toHaveBeenCalledTimes(2); + }); + + test('should track dynamic page view with malformed location info', async () => { + mockConfig.pageCounter = 0; + + const amplitude = createInstance(); + const track = jest.spyOn(amplitude, 'track').mockReturnValue({ + promise: Promise.resolve({ + code: 200, + message: '', + event: { + event_type: '[Amplitude] Page Viewed', + }, + }), + }); + + const oldURL = new URL('https://www.example.com'); + mockWindowLocationFromURL(oldURL); + const plugin = pageViewTrackingPlugin(); + await plugin.setup?.(mockConfig, amplitude); + + const malformedPath = '/home-%D1%88%D0%B5%D0BB%D0%BB%D1%8B'; // Invalid encoding string + const malformedURL = `https://www.example.com${malformedPath}`; + const malformedLocation = `https://www.example.com${malformedPath}?x=test`; + const newURL = new URL(malformedLocation); + mockWindowLocationFromURL(newURL); + window.history.pushState(undefined, newURL.href); + + // Page view tracking on push state executes async + // Block event loop for 1s before asserting + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(track).toHaveBeenNthCalledWith(1, { + event_properties: { + '[Amplitude] Page Domain': oldURL.hostname, + '[Amplitude] Page Location': oldURL.toString(), + '[Amplitude] Page Path': oldURL.pathname, + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': oldURL.toString(), + }, + event_type: '[Amplitude] Page Viewed', + }); + + expect(track).toHaveBeenNthCalledWith(2, { + event_properties: { + '[Amplitude] Page Domain': newURL.hostname, + '[Amplitude] Page Location': malformedLocation, + '[Amplitude] Page Path': malformedPath, + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': malformedURL, + }, + event_type: '[Amplitude] Page Viewed', + }); + + expect(track).toHaveBeenCalledTimes(2); + }); + + test('should track dynamic page view with regular location info', async () => { + mockConfig.pageCounter = 0; + + const amplitude = createInstance(); + const track = jest.spyOn(amplitude, 'track').mockReturnValue({ + promise: Promise.resolve({ + code: 200, + message: '', + event: { + event_type: '[Amplitude] Page Viewed', + }, + }), + }); + + const oldURL = new URL('https://www.example.com'); + mockWindowLocationFromURL(oldURL); + const plugin = pageViewTrackingPlugin(); + await plugin.setup?.(mockConfig, amplitude); + + const newBaseURL = `https://www.example.com/home-shell`; + const newURL = new URL(`${newBaseURL}?x=test`); + mockWindowLocationFromURL(newURL); + window.history.pushState(undefined, newURL.href); + + // Page view tracking on push state executes async + // Block event loop for 1s before asserting + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(track).toHaveBeenNthCalledWith(1, { + event_properties: { + '[Amplitude] Page Domain': oldURL.hostname, + '[Amplitude] Page Location': oldURL.toString(), + '[Amplitude] Page Path': oldURL.pathname, + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': oldURL.toString(), + }, + event_type: '[Amplitude] Page Viewed', + }); + + expect(track).toHaveBeenNthCalledWith(2, { + event_properties: { + '[Amplitude] Page Domain': newURL.hostname, + '[Amplitude] Page Location': newURL.toString(), + '[Amplitude] Page Path': newURL.pathname, + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': newBaseURL, + }, + event_type: '[Amplitude] Page Viewed', + }); + + expect(track).toHaveBeenCalledTimes(2); + }); }); describe('execute', () => {