UNPKG

@aws-amplify/analytics

Version:

Analytics category of aws-amplify

168 lines (166 loc) 6.59 kB
'use strict'; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 Object.defineProperty(exports, "__esModule", { value: true }); exports.PageViewTracker = void 0; const core_1 = require("@aws-amplify/core"); const utils_1 = require("@aws-amplify/core/internals/utils"); const logger = new core_1.ConsoleLogger('PageViewTracker'); const DEFAULT_EVENT_NAME = 'pageView'; const DEFAULT_APP_TYPE = 'singlePage'; const DEFAULT_URL_PROVIDER = () => { return window.location.origin + window.location.pathname; }; const PREV_URL_STORAGE_KEY = 'aws-amplify-analytics-prevUrl'; /** * Custom event broadcast by the shared history patch (see below) whenever * `history.pushState` / `history.replaceState` is invoked. Every active * `PageViewTracker` subscribes to it, so a single global patch can drive any * number of concurrent tracker instances. */ const SPA_NAVIGATION_EVENT = 'amplify-analytics-spa-navigation'; /** * Reference-counted, process-global patch of the History API. * * `PageViewTracker` instances previously each installed their own revocable * proxy on `window.history.pushState` / `replaceState`. That is not safe once * more than one instance can exist at a time (e.g. `pageView` auto-track enabled * on both Pinpoint and Kinesis): the proxies stacked (each instance captured the * previous instance's proxy as its "original"), so only the last-installed * instance actually received SPA navigations, and disabling providers in the * wrong order could leave a revoked proxy installed — throwing on the next * `pushState`. * * Instead the History API is patched exactly once (guarded by a reference * count). The patch simply forwards to the native implementation and broadcasts * {@link SPA_NAVIGATION_EVENT}; each tracker instance listens for that event. * The patch is removed and the native methods restored only when the last * instance tears down, so any number of trackers can be enabled / disabled in * any order safely. */ let historyPatchRefCount = 0; let nativePushState; let nativeReplaceState; const installHistoryPatch = () => { historyPatchRefCount += 1; // The patch is shared across all instances; only install it once. if (historyPatchRefCount > 1) { return; } nativePushState = window.history.pushState; nativeReplaceState = window.history.replaceState; const dispatchNavigation = () => { window.dispatchEvent(new Event(SPA_NAVIGATION_EVENT)); }; window.history.pushState = function pushState(...args) { nativePushState.apply(this, args); dispatchNavigation(); }; window.history.replaceState = function replaceState(...args) { nativeReplaceState.apply(this, args); dispatchNavigation(); }; }; const uninstallHistoryPatch = () => { if (historyPatchRefCount === 0) { return; } historyPatchRefCount -= 1; // Keep the patch installed while other instances still rely on it. if (historyPatchRefCount > 0) { return; } if (nativePushState) { window.history.pushState = nativePushState; nativePushState = undefined; } if (nativeReplaceState) { window.history.replaceState = nativeReplaceState; nativeReplaceState = undefined; } }; class PageViewTracker { constructor(eventRecorder, options, namespace) { this.options = {}; this.trackerActive = true; this.eventRecorder = eventRecorder; this.spaTrackingActive = false; this.storageKey = namespace ? `${PREV_URL_STORAGE_KEY}-${namespace}` : PREV_URL_STORAGE_KEY; this.handleLocationChange = this.handleLocationChange.bind(this); this.configure(eventRecorder, options); } configure(eventRecorder, options) { this.eventRecorder = eventRecorder; // Clean up any existing listeners this.cleanup(); // Apply defaults this.options = { appType: options?.appType ?? DEFAULT_APP_TYPE, attributes: options?.attributes ?? undefined, eventName: options?.eventName ?? DEFAULT_EVENT_NAME, urlProvider: options?.urlProvider ?? DEFAULT_URL_PROVIDER, }; // Configure SPA or MPA page view tracking if ((0, utils_1.isBrowser)()) { if (this.options.appType === 'singlePage') { this.setupSPATracking(); } else { this.setupMPATracking(); } this.trackerActive = true; } } cleanup() { // No-op if document listener is not active if (!this.trackerActive) { return; } // Clean up SPA page view listeners if (this.spaTrackingActive) { window.removeEventListener(SPA_NAVIGATION_EVENT, this.handleLocationChange); window.removeEventListener('popstate', this.handleLocationChange); uninstallHistoryPatch(); this.spaTrackingActive = false; } } setupSPATracking() { if (!this.spaTrackingActive) { // Subscribe to the shared history patch (installed once, ref-counted) // plus native `popstate`. Both funnel through `handleLocationChange`. installHistoryPatch(); window.addEventListener(SPA_NAVIGATION_EVENT, this.handleLocationChange); window.addEventListener('popstate', this.handleLocationChange); sessionStorage.removeItem(this.storageKey); this.spaTrackingActive = true; } } setupMPATracking() { this.handleLocationChange(); } handleLocationChange() { const currentUrl = this.options.urlProvider(); const eventName = this.options.eventName || DEFAULT_EVENT_NAME; if (this.urlHasChanged()) { sessionStorage.setItem(this.storageKey, currentUrl); // Assemble attribute list const attributes = Object.assign({ url: currentUrl, }, this.options.attributes); logger.debug('Recording automatically tracked page view event', { eventName, attributes, }); this.eventRecorder(eventName, attributes); } } urlHasChanged() { const prevUrl = sessionStorage.getItem(this.storageKey); const currUrl = this.options.urlProvider(); return currUrl !== prevUrl; } } exports.PageViewTracker = PageViewTracker; //# sourceMappingURL=PageViewTracker.js.map