Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: added fn configureDefaults() to up-ga.js #2849

Merged
merged 6 commits into from
Sep 20, 2024

Conversation

bjagg
Copy link
Member

@bjagg bjagg commented Sep 14, 2024

Checklist
Description of change

Default GA configuration is not being set. This change adds setting the defaults as global values.

Use of archaic JS compressor prevents use of modern ES6. Will be addressed in uPortal v6, but for now the code was rewritten in an older style that matches the rest of the file.

https://developers.google.com/analytics/devguides/collection/ga4/reference/config#:~:text=Set%20to%20'auto'%20(the,example.com%20for%20the%20domain.

Copy link
Member

@ChristianMurphy ChristianMurphy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks @bjagg!

* Set the defaultConfig.config array as global settings
*/
var configureDefaults = function (propertyConfig) {
//console.log(propertyConfig);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the commented out logs?
Could we remove them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question and one I anticipated. I think some of the clients will need to leverage the console logs to conform their configuration is correct. I can go either way. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be okay adding some info and warning logs.
Maybe adding some around the null checks to let adopters know configuration may be missing, and info at some key points?
Also if this is an area we expect adopters to review more, we could also beef up the JSDocs a bit to give more context.

/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
var uportal = uportal || {};

(function ($) {
    /**
     * Finds the appropriate property configuration for the current institution.
     * @returns {Object|null} The property configuration object or null if not found.
     */
    var findPropertyConfig = function () {
        if (up.analytics.model == null) {
            console.error("[Analytics] No analytics model found.");
            return null;
        }

        if (Array.isArray(up.analytics.model.hosts)) {
            var hosts = up.analytics.model.hosts;
            var propertyConfig = null;
            for (var i = 0; i < hosts.length; i++) {
                var propConfig = hosts[i];
                if (propConfig.name == up.analytics.host) {
                    propertyConfig = propConfig;
                    break;
                }
            }

            if (propertyConfig != null) {
                console.info("[Analytics] Found property configuration for host:", up.analytics.host);
                return propertyConfig;
            }
        }

        console.info("[Analytics] Using default property configuration.");
        return up.analytics.model.defaultConfig;
    };

    /**
     * Sets global settings from the property configuration.
     * @param {Object} propertyConfig - The property configuration object.
     */
    var configureDefaults = function (propertyConfig) {
        var defaults = propertyConfig.config || [];
        defaults.forEach(function (setting) {
            Object.keys(setting).forEach(function (key) {
                up.gtag('set', key, setting[key]);
            });
        });
    };

    /**
     * Retrieves dimensions that apply to the current user.
     * @param {Object} propertyConfig - The property configuration object.
     * @returns {Object} An object containing dimension key-value pairs.
     */
    var getDimensions = function (propertyConfig) {
        var dimensions = {};
        var dimensionGroups = propertyConfig.dimensionGroups || [];
        dimensionGroups.forEach(function (setting) {
            dimensions['dimension' + setting.name] = setting.value;
        });
        console.info("[Analytics] Dimensions set:", dimensions);
        return dimensions;
    };

    /**
     * Creates the Google Analytics tracker with the specified property ID and configuration.
     * @param {Object} propertyConfig - The property configuration object.
     */
    var createTracker = function (propertyConfig) {
        var createSettings = {};
        var configSettings = propertyConfig.config || [];
        configSettings.forEach(function (setting) {
            if (setting.name != 'name') {
                createSettings[setting.name] = setting.value;
            }
        });
        up.gtag('config', propertyConfig.propertyId, {
            send_page_view: false,
        });
        console.info("[Analytics] Tracker created with property ID:", propertyConfig.propertyId);
    };

    /**
     * Builds the page URI for a tab.
     * @param {string} [fragmentName] - The fragment name.
     * @param {string} [tabName] - The tab name.
     * @returns {string} The constructed tab URI.
     */
    var getTabUri = function (fragmentName, tabName) {
        if (up.analytics.pageData.tab != null) {
            fragmentName =
                fragmentName || up.analytics.pageData.tab.fragmentName;
            tabName = tabName || up.analytics.pageData.tab.tabName;
        }

        var uri = '/';

        if (fragmentName != null) {
            uri += 'tab/' + fragmentName;

            if (tabName != null) {
                uri += '/' + tabName;
            }
        }

        return uri;
    };

    /**
     * Retrieves variables specific to the current page.
     * @param {string} [fragmentName] - The fragment name.
     * @param {string} [tabName] - The tab name.
     * @returns {Object} An object containing page variables.
     */
    var getPageVariables = function (fragmentName, tabName) {
        if (up.analytics.pageData.tab != null) {
            fragmentName =
                fragmentName || up.analytics.pageData.tab.fragmentName;
            tabName = tabName || up.analytics.pageData.tab.tabName;
        }

        var title;
        if (tabName != null) {
            title = 'Tab: ' + tabName;
        } else if (up.analytics.pageData.urlState == null) {
            title = 'Portal Home';
        } else {
            title = 'No Tab';
        }
        return {
            page_location: getTabUri(fragmentName, tabName),
            page_title: title,
        };
    };

    /**
     * Safely resolves the portlet's fname from the windowId.
     * Falls back to using the windowId if no fname is found.
     * @param {string} windowId - The window ID of the portlet.
     * @returns {string} The fname of the portlet.
     */
    var getPortletFname = function (windowId) {
        var portletData = up.analytics.portletData[windowId];
        if (portletData == null) {
            return windowId;
        }

        return portletData.fname;
    };

    /**
     * Safely resolves the portlet's title from the windowId.
     * Falls back to using getPortletFname(windowId) if the title can't be found.
     * @param {string} windowId - The window ID of the portlet.
     * @returns {string} The title of the portlet.
     */
    var getRenderedPortletTitle = function (windowId) {
        var portletWindowWrapper = $(
            'div.up-portlet-windowId-content-wrapper.' + windowId
        );
        if (portletWindowWrapper.length == 0) {
            return getPortletFname(windowId);
        }

        var portletWrapper = portletWindowWrapper.parents(
            'div.up-portlet-wrapper-inner'
        );
        if (portletWrapper.length == 0) {
            return getPortletFname(windowId);
        }

        var portletTitle = portletWrapper.find('div.up-portlet-titlebar h2 a');
        if (portletTitle.length == 0) {
            return getPortletFname(windowId);
        }

        return portletTitle.text().trim();
    };

    /**
     * Builds the portlet URI for the specified portlet.
     * @param {string} fname - The fname of the portlet.
     * @returns {string} The constructed portlet URI.
     */
    var getPortletUri = function (fname) {
        return '/portlet/' + fname;
    };

    /**
     * Retrieves variables specific to the specified portlet.
     * @param {string} windowId - The window ID of the portlet.
     * @param {Object} [portletData] - The data object of the portlet.
     * @returns {Object} An object containing portlet variables.
     */
    var getPortletVariables = function (windowId, portletData) {
        var portletTitle = getRenderedPortletTitle(windowId);

        if (portletData == null) {
            portletData = up.analytics.portletData[windowId];
        }
        return {
            page_title: 'Portlet: ' + portletTitle,
            page_location: getPortletUri(portletData.fname),
        };
    };

    /**
     * Retrieves the first class name from an element that is not in the excludedClasses array.
     * @param {Function} selectorFunction - A function that returns a jQuery element.
     * @param {string|string[]} excludedClasses - Class name or array of class names to exclude.
     * @returns {string|null} The first class name not in excludedClasses, or null if none found.
     */
    var getInfoClass = function (selectorFunction, excludedClasses) {
        // Ensure excludedClasses is an array
        if (!Array.isArray(excludedClasses)) {
            excludedClasses = [excludedClasses];
        }

        var classAttribute = selectorFunction().attr('class');
        if (classAttribute == null) {
            return null;
        }

        var classes = classAttribute.split(/\s+/);
        for (var i = 0; i < classes.length; i++) {
            var cls = classes[i];
            if (excludedClasses.indexOf(cls) === -1) {
                return cls;
            }
        }
        return null;
    };

    /**
     * Determines the fname of the portlet the clicked flyout was rendered for.
     * @param {Object} clickedLink - The jQuery object representing the clicked link.
     * @returns {string|null} The fname of the portlet, or null if not found.
     */
    var getFlyoutFname = function (clickedLink) {
        return getInfoClass(function () {
            return clickedLink.parents('div.up-portlet-fname-subnav-wrapper');
        }, 'up-portlet-fname-subnav-wrapper');
    };

    /**
     * Determines the windowId of the portlet the clicked external link was rendered for.
     * @param {Object} clickedLink - The jQuery object representing the clicked link.
     * @returns {string|null} The windowId of the portlet, or null if not found.
     */
    var getExternalLinkWindowId = function (clickedLink) {
        return getInfoClass(function () {
            return clickedLink.parents(
                'div.up-portlet-windowId-content-wrapper'
            );
        }, 'up-portlet-windowId-content-wrapper');
    };

    /**
     * Handles link click events for analytics tracking.
     * Sends an analytics event and manages navigation behavior.
     * @param {Object} event - The jQuery event object.
     * @param {Object} clickedLink - The jQuery object representing the clicked link.
     * @param {Object} eventOptions - Additional options for the analytics event.
     */
    var handleLinkClickEvent = function (event, clickedLink, eventOptions) {
        // Determine if the click will open in a new window
        var newWindow =
            event.button == 1 ||
            event.metaKey ||
            event.ctrlKey ||
            clickedLink.attr('target') != null;

        var clickFunction;
        clickFunction = newWindow
            ? function () {}
            : function () {
                  document.location = clickedLink.attr('href');
              };

        up.gtag(
            'event',
            'page_view',
            $.extend(
                {
                    event_callback: clickFunction,
                },
                eventOptions
            )
        );

        // If not opening a new window, prevent default behavior and set a fallback
        if (!newWindow) {
            // Fallback in case event_callback is not called promptly
            setTimeout(clickFunction, 200);

            event.preventDefault();
        }
    };

    /**
     * Adds click handlers to flyout menus to fire analytics events when used.
     */
    var addFlyoutHandlers = function () {
        $('ul.fl-tabs li.portal-navigation a.portal-subnav-link').click(
            function (event) {
                var clickedLink = $(this);

                // Get the target portlet's title
                var portletFlyoutTitle = clickedLink
                    .find('span.portal-subnav-label')
                    .text();

                // Get the target portlet's fname
                var fname = getFlyoutFname(clickedLink);

                // Setup page-level variables
                var pageVariables = getPageVariables();

                // Send the analytics event and handle the click
                handleLinkClickEvent(
                    event,
                    clickedLink,
                    $.extend(
                        {
                            event_category: 'Flyout Link',
                            event_action: getPortletUri(fname),
                            event_label: portletFlyoutTitle,
                        },
                        pageVariables
                    )
                );
            }
        );
    };

    /**
     * Adds handlers to inspect clicks on links and track outbound link events.
     */
    var addExternalLinkHandlers = function () {
        $('a').click(function (event) {
            var clickedLink = $(this);

            var linkHost = clickedLink.prop('hostname');
            if (linkHost != '' && linkHost != document.domain) {
                var windowId = getExternalLinkWindowId(clickedLink);
                var eventVariables = null;
                eventVariables =
                    windowId == null
                        ? getPageVariables()
                        : getPortletVariables(windowId);

                // Send the analytics event and handle the click
                handleLinkClickEvent(
                    event,
                    clickedLink,
                    $.extend(
                        {
                            event_category: 'Outbound Link',
                            event_action: clickedLink.prop('href'),
                            event_label: clickedLink.text(),
                        },
                        eventVariables
                    )
                );
            }
        });
    };

    /**
     * Adds handlers to track "tab" clicks in the mobile accordion view.
     */
    var addMobileListTabHandlers = function () {
        $('ul.up-portal-nav li.up-tab').click(function () {
            var clickedTab = $(this);

            // Ignore clicks on already open tabs
            if (clickedTab.hasClass('up-tab-open')) {
                return;
            }

            var fragmentName = getInfoClass(function () {
                return clickedTab.find('div.up-tab-owner');
            }, 'up-tab-owner');

            var tabName = clickedTab.find('span.up-tab-name').text().trim();

            var pageVariables = getPageVariables(fragmentName, tabName);

            up.gtag('event', 'page_view', pageVariables);
        });
    };

    $(document).ready(function () {
        // Initialize property configuration
        var propertyConfig = findPropertyConfig();

        // No property config means analytics cannot proceed
        if (propertyConfig == null) {
            console.error("[Analytics] No property configuration found. Analytics will not be initialized.");
            return;
        }

        // Set default configuration
        configureDefaults(propertyConfig);

        // Create the tracker
        createTracker(propertyConfig);

        // Set dimensions for the current user
        var dimensions = getDimensions(propertyConfig);

        up.gtag('event', 'page_view', dimensions);

        // Prepare page-level variables
        var pageVariables = getPageVariables();

        // Send page view event unless in MAX WindowState
        if (up.analytics.pageData.urlState != 'MAX') {
            up.gtag('event', 'page_view', $.extend(pageVariables, dimensions));
        }

        // Send timing event for page load
        up.gtag(
            'event',
            'timing_complete',
            $.extend(
                {
                    event_category: 'tab',
                    name: getTabUri(),
                    value: up.analytics.pageData.executionTimeNano,
                },
                pageVariables,
                dimensions
            )
        );

        // Send events for each portlet
        for (var windowId in up.analytics.portletData) {
            if (up.analytics.portletData.hasOwnProperty(windowId)) {
                var portletData = up.analytics.portletData[windowId];
                // Skip excluded portlets
                if (portletData.fname == 'google-analytics-config') {
                    console.info("[Analytics] Skipping portlet:", portletData.fname);
                    continue;
                }

                var portletVariables = getPortletVariables(windowId, portletData);
                up.gtag('event', 'page_view', portletVariables);
                up.gtag(
                    'event',
                    'timing_complete',
                    $.extend(
                        {
                            event_category: 'tab',
                            name: getTabUri(),
                            value: up.analytics.pageData.executionTimeNano,
                        },
                        portletVariables,
                        dimensions
                    )
                );
            }
        }

        // Add event handlers
        addFlyoutHandlers();
        addExternalLinkHandlers();
        addMobileListTabHandlers();
    });
})(jQuery);

* Set the defaultConfig.config array as global settings
*/
var configureDefaults = function (propertyConfig) {
//console.log(propertyConfig);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be okay adding some info and warning logs.
Maybe adding some around the null checks to let adopters know configuration may be missing, and info at some key points?
Also if this is an area we expect adopters to review more, we could also beef up the JSDocs a bit to give more context.

/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
var uportal = uportal || {};

(function ($) {
    /**
     * Finds the appropriate property configuration for the current institution.
     * @returns {Object|null} The property configuration object or null if not found.
     */
    var findPropertyConfig = function () {
        if (up.analytics.model == null) {
            console.error("[Analytics] No analytics model found.");
            return null;
        }

        if (Array.isArray(up.analytics.model.hosts)) {
            var hosts = up.analytics.model.hosts;
            var propertyConfig = null;
            for (var i = 0; i < hosts.length; i++) {
                var propConfig = hosts[i];
                if (propConfig.name == up.analytics.host) {
                    propertyConfig = propConfig;
                    break;
                }
            }

            if (propertyConfig != null) {
                console.info("[Analytics] Found property configuration for host:", up.analytics.host);
                return propertyConfig;
            }
        }

        console.info("[Analytics] Using default property configuration.");
        return up.analytics.model.defaultConfig;
    };

    /**
     * Sets global settings from the property configuration.
     * @param {Object} propertyConfig - The property configuration object.
     */
    var configureDefaults = function (propertyConfig) {
        var defaults = propertyConfig.config || [];
        defaults.forEach(function (setting) {
            Object.keys(setting).forEach(function (key) {
                up.gtag('set', key, setting[key]);
            });
        });
    };

    /**
     * Retrieves dimensions that apply to the current user.
     * @param {Object} propertyConfig - The property configuration object.
     * @returns {Object} An object containing dimension key-value pairs.
     */
    var getDimensions = function (propertyConfig) {
        var dimensions = {};
        var dimensionGroups = propertyConfig.dimensionGroups || [];
        dimensionGroups.forEach(function (setting) {
            dimensions['dimension' + setting.name] = setting.value;
        });
        console.info("[Analytics] Dimensions set:", dimensions);
        return dimensions;
    };

    /**
     * Creates the Google Analytics tracker with the specified property ID and configuration.
     * @param {Object} propertyConfig - The property configuration object.
     */
    var createTracker = function (propertyConfig) {
        var createSettings = {};
        var configSettings = propertyConfig.config || [];
        configSettings.forEach(function (setting) {
            if (setting.name != 'name') {
                createSettings[setting.name] = setting.value;
            }
        });
        up.gtag('config', propertyConfig.propertyId, {
            send_page_view: false,
        });
        console.info("[Analytics] Tracker created with property ID:", propertyConfig.propertyId);
    };

    /**
     * Builds the page URI for a tab.
     * @param {string} [fragmentName] - The fragment name.
     * @param {string} [tabName] - The tab name.
     * @returns {string} The constructed tab URI.
     */
    var getTabUri = function (fragmentName, tabName) {
        if (up.analytics.pageData.tab != null) {
            fragmentName =
                fragmentName || up.analytics.pageData.tab.fragmentName;
            tabName = tabName || up.analytics.pageData.tab.tabName;
        }

        var uri = '/';

        if (fragmentName != null) {
            uri += 'tab/' + fragmentName;

            if (tabName != null) {
                uri += '/' + tabName;
            }
        }

        return uri;
    };

    /**
     * Retrieves variables specific to the current page.
     * @param {string} [fragmentName] - The fragment name.
     * @param {string} [tabName] - The tab name.
     * @returns {Object} An object containing page variables.
     */
    var getPageVariables = function (fragmentName, tabName) {
        if (up.analytics.pageData.tab != null) {
            fragmentName =
                fragmentName || up.analytics.pageData.tab.fragmentName;
            tabName = tabName || up.analytics.pageData.tab.tabName;
        }

        var title;
        if (tabName != null) {
            title = 'Tab: ' + tabName;
        } else if (up.analytics.pageData.urlState == null) {
            title = 'Portal Home';
        } else {
            title = 'No Tab';
        }
        return {
            page_location: getTabUri(fragmentName, tabName),
            page_title: title,
        };
    };

    /**
     * Safely resolves the portlet's fname from the windowId.
     * Falls back to using the windowId if no fname is found.
     * @param {string} windowId - The window ID of the portlet.
     * @returns {string} The fname of the portlet.
     */
    var getPortletFname = function (windowId) {
        var portletData = up.analytics.portletData[windowId];
        if (portletData == null) {
            return windowId;
        }

        return portletData.fname;
    };

    /**
     * Safely resolves the portlet's title from the windowId.
     * Falls back to using getPortletFname(windowId) if the title can't be found.
     * @param {string} windowId - The window ID of the portlet.
     * @returns {string} The title of the portlet.
     */
    var getRenderedPortletTitle = function (windowId) {
        var portletWindowWrapper = $(
            'div.up-portlet-windowId-content-wrapper.' + windowId
        );
        if (portletWindowWrapper.length == 0) {
            return getPortletFname(windowId);
        }

        var portletWrapper = portletWindowWrapper.parents(
            'div.up-portlet-wrapper-inner'
        );
        if (portletWrapper.length == 0) {
            return getPortletFname(windowId);
        }

        var portletTitle = portletWrapper.find('div.up-portlet-titlebar h2 a');
        if (portletTitle.length == 0) {
            return getPortletFname(windowId);
        }

        return portletTitle.text().trim();
    };

    /**
     * Builds the portlet URI for the specified portlet.
     * @param {string} fname - The fname of the portlet.
     * @returns {string} The constructed portlet URI.
     */
    var getPortletUri = function (fname) {
        return '/portlet/' + fname;
    };

    /**
     * Retrieves variables specific to the specified portlet.
     * @param {string} windowId - The window ID of the portlet.
     * @param {Object} [portletData] - The data object of the portlet.
     * @returns {Object} An object containing portlet variables.
     */
    var getPortletVariables = function (windowId, portletData) {
        var portletTitle = getRenderedPortletTitle(windowId);

        if (portletData == null) {
            portletData = up.analytics.portletData[windowId];
        }
        return {
            page_title: 'Portlet: ' + portletTitle,
            page_location: getPortletUri(portletData.fname),
        };
    };

    /**
     * Retrieves the first class name from an element that is not in the excludedClasses array.
     * @param {Function} selectorFunction - A function that returns a jQuery element.
     * @param {string|string[]} excludedClasses - Class name or array of class names to exclude.
     * @returns {string|null} The first class name not in excludedClasses, or null if none found.
     */
    var getInfoClass = function (selectorFunction, excludedClasses) {
        // Ensure excludedClasses is an array
        if (!Array.isArray(excludedClasses)) {
            excludedClasses = [excludedClasses];
        }

        var classAttribute = selectorFunction().attr('class');
        if (classAttribute == null) {
            return null;
        }

        var classes = classAttribute.split(/\s+/);
        for (var i = 0; i < classes.length; i++) {
            var cls = classes[i];
            if (excludedClasses.indexOf(cls) === -1) {
                return cls;
            }
        }
        return null;
    };

    /**
     * Determines the fname of the portlet the clicked flyout was rendered for.
     * @param {Object} clickedLink - The jQuery object representing the clicked link.
     * @returns {string|null} The fname of the portlet, or null if not found.
     */
    var getFlyoutFname = function (clickedLink) {
        return getInfoClass(function () {
            return clickedLink.parents('div.up-portlet-fname-subnav-wrapper');
        }, 'up-portlet-fname-subnav-wrapper');
    };

    /**
     * Determines the windowId of the portlet the clicked external link was rendered for.
     * @param {Object} clickedLink - The jQuery object representing the clicked link.
     * @returns {string|null} The windowId of the portlet, or null if not found.
     */
    var getExternalLinkWindowId = function (clickedLink) {
        return getInfoClass(function () {
            return clickedLink.parents(
                'div.up-portlet-windowId-content-wrapper'
            );
        }, 'up-portlet-windowId-content-wrapper');
    };

    /**
     * Handles link click events for analytics tracking.
     * Sends an analytics event and manages navigation behavior.
     * @param {Object} event - The jQuery event object.
     * @param {Object} clickedLink - The jQuery object representing the clicked link.
     * @param {Object} eventOptions - Additional options for the analytics event.
     */
    var handleLinkClickEvent = function (event, clickedLink, eventOptions) {
        // Determine if the click will open in a new window
        var newWindow =
            event.button == 1 ||
            event.metaKey ||
            event.ctrlKey ||
            clickedLink.attr('target') != null;

        var clickFunction;
        clickFunction = newWindow
            ? function () {}
            : function () {
                  document.location = clickedLink.attr('href');
              };

        up.gtag(
            'event',
            'page_view',
            $.extend(
                {
                    event_callback: clickFunction,
                },
                eventOptions
            )
        );

        // If not opening a new window, prevent default behavior and set a fallback
        if (!newWindow) {
            // Fallback in case event_callback is not called promptly
            setTimeout(clickFunction, 200);

            event.preventDefault();
        }
    };

    /**
     * Adds click handlers to flyout menus to fire analytics events when used.
     */
    var addFlyoutHandlers = function () {
        $('ul.fl-tabs li.portal-navigation a.portal-subnav-link').click(
            function (event) {
                var clickedLink = $(this);

                // Get the target portlet's title
                var portletFlyoutTitle = clickedLink
                    .find('span.portal-subnav-label')
                    .text();

                // Get the target portlet's fname
                var fname = getFlyoutFname(clickedLink);

                // Setup page-level variables
                var pageVariables = getPageVariables();

                // Send the analytics event and handle the click
                handleLinkClickEvent(
                    event,
                    clickedLink,
                    $.extend(
                        {
                            event_category: 'Flyout Link',
                            event_action: getPortletUri(fname),
                            event_label: portletFlyoutTitle,
                        },
                        pageVariables
                    )
                );
            }
        );
    };

    /**
     * Adds handlers to inspect clicks on links and track outbound link events.
     */
    var addExternalLinkHandlers = function () {
        $('a').click(function (event) {
            var clickedLink = $(this);

            var linkHost = clickedLink.prop('hostname');
            if (linkHost != '' && linkHost != document.domain) {
                var windowId = getExternalLinkWindowId(clickedLink);
                var eventVariables = null;
                eventVariables =
                    windowId == null
                        ? getPageVariables()
                        : getPortletVariables(windowId);

                // Send the analytics event and handle the click
                handleLinkClickEvent(
                    event,
                    clickedLink,
                    $.extend(
                        {
                            event_category: 'Outbound Link',
                            event_action: clickedLink.prop('href'),
                            event_label: clickedLink.text(),
                        },
                        eventVariables
                    )
                );
            }
        });
    };

    /**
     * Adds handlers to track "tab" clicks in the mobile accordion view.
     */
    var addMobileListTabHandlers = function () {
        $('ul.up-portal-nav li.up-tab').click(function () {
            var clickedTab = $(this);

            // Ignore clicks on already open tabs
            if (clickedTab.hasClass('up-tab-open')) {
                return;
            }

            var fragmentName = getInfoClass(function () {
                return clickedTab.find('div.up-tab-owner');
            }, 'up-tab-owner');

            var tabName = clickedTab.find('span.up-tab-name').text().trim();

            var pageVariables = getPageVariables(fragmentName, tabName);

            up.gtag('event', 'page_view', pageVariables);
        });
    };

    $(document).ready(function () {
        // Initialize property configuration
        var propertyConfig = findPropertyConfig();

        // No property config means analytics cannot proceed
        if (propertyConfig == null) {
            console.error("[Analytics] No property configuration found. Analytics will not be initialized.");
            return;
        }

        // Set default configuration
        configureDefaults(propertyConfig);

        // Create the tracker
        createTracker(propertyConfig);

        // Set dimensions for the current user
        var dimensions = getDimensions(propertyConfig);

        up.gtag('event', 'page_view', dimensions);

        // Prepare page-level variables
        var pageVariables = getPageVariables();

        // Send page view event unless in MAX WindowState
        if (up.analytics.pageData.urlState != 'MAX') {
            up.gtag('event', 'page_view', $.extend(pageVariables, dimensions));
        }

        // Send timing event for page load
        up.gtag(
            'event',
            'timing_complete',
            $.extend(
                {
                    event_category: 'tab',
                    name: getTabUri(),
                    value: up.analytics.pageData.executionTimeNano,
                },
                pageVariables,
                dimensions
            )
        );

        // Send events for each portlet
        for (var windowId in up.analytics.portletData) {
            if (up.analytics.portletData.hasOwnProperty(windowId)) {
                var portletData = up.analytics.portletData[windowId];
                // Skip excluded portlets
                if (portletData.fname == 'google-analytics-config') {
                    console.info("[Analytics] Skipping portlet:", portletData.fname);
                    continue;
                }

                var portletVariables = getPortletVariables(windowId, portletData);
                up.gtag('event', 'page_view', portletVariables);
                up.gtag(
                    'event',
                    'timing_complete',
                    $.extend(
                        {
                            event_category: 'tab',
                            name: getTabUri(),
                            value: up.analytics.pageData.executionTimeNano,
                        },
                        portletVariables,
                        dimensions
                    )
                );
            }
        }

        // Add event handlers
        addFlyoutHandlers();
        addExternalLinkHandlers();
        addMobileListTabHandlers();
    });
})(jQuery);

@mhuffsti
Copy link

@bjagg I went ahead and did a quick functional test. I focused on cookie_domain which is what started all of this, and it worked as expected this time with the updated JS file.

@jgribonvald
Copy link
Contributor

Hi folks, just my 2 cents from european side. Our Protection Data Officers doesn't want to see any Google analytics scripts loaded like initialized even if the configuration is empty. It would be great if this part would be inside an optional stuff, like a jsp portlet code that we won't load by removing the portlet def. I have the same problem with using some CDN links like for google fonts. Google has trackers on.
Analytics are usefull, but it should be managed at a custom way, with an organization platform. So that we host the apps that collect datas and that we provide anonymous stats. We can find alternatives to GA without problems 😉

@ChristianMurphy
Copy link
Member

Analytics are usefull, but it should be managed at a custom way, with an organization platform. So that we host the apps that collect datas and that we provide anonymous stats. We can find alternatives to GA without problems 😉

Related, I've had pretty good experiences with Matomo https://matomo.org/
Which is both self-host-able and GDPR compliant.

@jgribonvald
Copy link
Contributor

Analytics are usefull, but it should be managed at a custom way, with an organization platform. So that we host the apps that collect datas and that we provide anonymous stats. We can find alternatives to GA without problems 😉

Related, I've had pretty good experiences with Matomo https://matomo.org/
Which is both self-host-able and GDPR compliant.

Matomo is largely deployed in France ;)

@bjagg
Copy link
Member Author

bjagg commented Sep 20, 2024

This PR is focused on a configuration omission. Thanks for the code, @ChristianMurphy.

I'll open an issue about making GA optional and having other solutions.

You guys are awesome!

@bjagg bjagg mentioned this pull request Sep 20, 2024
@bjagg
Copy link
Member Author

bjagg commented Sep 20, 2024

Yes, looks like the linter and the compressor conflict. Going to revert to the previous version and capture this suggestion as a future issue once we upgrade the JS tooling.

@bjagg bjagg merged commit f4af51a into uPortal-Project:master Sep 20, 2024
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants