| 1 | /*
|
|---|
| 2 | Copyright 2018 Google LLC
|
|---|
| 3 |
|
|---|
| 4 | Use of this source code is governed by an MIT-style
|
|---|
| 5 | license that can be found in the LICENSE file or at
|
|---|
| 6 | https://opensource.org/licenses/MIT.
|
|---|
| 7 | */
|
|---|
| 8 | import { BackgroundSyncPlugin } from 'workbox-background-sync/BackgroundSyncPlugin.js';
|
|---|
| 9 | import { cacheNames } from 'workbox-core/_private/cacheNames.js';
|
|---|
| 10 | import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
|
|---|
| 11 | import { logger } from 'workbox-core/_private/logger.js';
|
|---|
| 12 | import { Route } from 'workbox-routing/Route.js';
|
|---|
| 13 | import { Router } from 'workbox-routing/Router.js';
|
|---|
| 14 | import { NetworkFirst } from 'workbox-strategies/NetworkFirst.js';
|
|---|
| 15 | import { NetworkOnly } from 'workbox-strategies/NetworkOnly.js';
|
|---|
| 16 | import { QUEUE_NAME, MAX_RETENTION_TIME, GOOGLE_ANALYTICS_HOST, GTM_HOST, ANALYTICS_JS_PATH, GTAG_JS_PATH, GTM_JS_PATH, COLLECT_PATHS_REGEX, } from './utils/constants.js';
|
|---|
| 17 | import './_version.js';
|
|---|
| 18 | /**
|
|---|
| 19 | * Creates the requestWillDequeue callback to be used with the background
|
|---|
| 20 | * sync plugin. The callback takes the failed request and adds the
|
|---|
| 21 | * `qt` param based on the current time, as well as applies any other
|
|---|
| 22 | * user-defined hit modifications.
|
|---|
| 23 | *
|
|---|
| 24 | * @param {Object} config See {@link workbox-google-analytics.initialize}.
|
|---|
| 25 | * @return {Function} The requestWillDequeue callback function.
|
|---|
| 26 | *
|
|---|
| 27 | * @private
|
|---|
| 28 | */
|
|---|
| 29 | const createOnSyncCallback = (config) => {
|
|---|
| 30 | return async ({ queue }) => {
|
|---|
| 31 | let entry;
|
|---|
| 32 | while ((entry = await queue.shiftRequest())) {
|
|---|
| 33 | const { request, timestamp } = entry;
|
|---|
| 34 | const url = new URL(request.url);
|
|---|
| 35 | try {
|
|---|
| 36 | // Measurement protocol requests can set their payload parameters in
|
|---|
| 37 | // either the URL query string (for GET requests) or the POST body.
|
|---|
| 38 | const params = request.method === 'POST'
|
|---|
| 39 | ? new URLSearchParams(await request.clone().text())
|
|---|
| 40 | : url.searchParams;
|
|---|
| 41 | // Calculate the qt param, accounting for the fact that an existing
|
|---|
| 42 | // qt param may be present and should be updated rather than replaced.
|
|---|
| 43 | const originalHitTime = timestamp - (Number(params.get('qt')) || 0);
|
|---|
| 44 | const queueTime = Date.now() - originalHitTime;
|
|---|
| 45 | // Set the qt param prior to applying hitFilter or parameterOverrides.
|
|---|
| 46 | params.set('qt', String(queueTime));
|
|---|
| 47 | // Apply `parameterOverrides`, if set.
|
|---|
| 48 | if (config.parameterOverrides) {
|
|---|
| 49 | for (const param of Object.keys(config.parameterOverrides)) {
|
|---|
| 50 | const value = config.parameterOverrides[param];
|
|---|
| 51 | params.set(param, value);
|
|---|
| 52 | }
|
|---|
| 53 | }
|
|---|
| 54 | // Apply `hitFilter`, if set.
|
|---|
| 55 | if (typeof config.hitFilter === 'function') {
|
|---|
| 56 | config.hitFilter.call(null, params);
|
|---|
| 57 | }
|
|---|
| 58 | // Retry the fetch. Ignore URL search params from the URL as they're
|
|---|
| 59 | // now in the post body.
|
|---|
| 60 | await fetch(new Request(url.origin + url.pathname, {
|
|---|
| 61 | body: params.toString(),
|
|---|
| 62 | method: 'POST',
|
|---|
| 63 | mode: 'cors',
|
|---|
| 64 | credentials: 'omit',
|
|---|
| 65 | headers: { 'Content-Type': 'text/plain' },
|
|---|
| 66 | }));
|
|---|
| 67 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 68 | logger.log(`Request for '${getFriendlyURL(url.href)}' ` + `has been replayed`);
|
|---|
| 69 | }
|
|---|
| 70 | }
|
|---|
| 71 | catch (err) {
|
|---|
| 72 | await queue.unshiftRequest(entry);
|
|---|
| 73 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 74 | logger.log(`Request for '${getFriendlyURL(url.href)}' ` +
|
|---|
| 75 | `failed to replay, putting it back in the queue.`);
|
|---|
| 76 | }
|
|---|
| 77 | throw err;
|
|---|
| 78 | }
|
|---|
| 79 | }
|
|---|
| 80 | if (process.env.NODE_ENV !== 'production') {
|
|---|
| 81 | logger.log(`All Google Analytics request successfully replayed; ` +
|
|---|
| 82 | `the queue is now empty!`);
|
|---|
| 83 | }
|
|---|
| 84 | };
|
|---|
| 85 | };
|
|---|
| 86 | /**
|
|---|
| 87 | * Creates GET and POST routes to catch failed Measurement Protocol hits.
|
|---|
| 88 | *
|
|---|
| 89 | * @param {BackgroundSyncPlugin} bgSyncPlugin
|
|---|
| 90 | * @return {Array<Route>} The created routes.
|
|---|
| 91 | *
|
|---|
| 92 | * @private
|
|---|
| 93 | */
|
|---|
| 94 | const createCollectRoutes = (bgSyncPlugin) => {
|
|---|
| 95 | const match = ({ url }) => url.hostname === GOOGLE_ANALYTICS_HOST &&
|
|---|
| 96 | COLLECT_PATHS_REGEX.test(url.pathname);
|
|---|
| 97 | const handler = new NetworkOnly({
|
|---|
| 98 | plugins: [bgSyncPlugin],
|
|---|
| 99 | });
|
|---|
| 100 | return [new Route(match, handler, 'GET'), new Route(match, handler, 'POST')];
|
|---|
| 101 | };
|
|---|
| 102 | /**
|
|---|
| 103 | * Creates a route with a network first strategy for the analytics.js script.
|
|---|
| 104 | *
|
|---|
| 105 | * @param {string} cacheName
|
|---|
| 106 | * @return {Route} The created route.
|
|---|
| 107 | *
|
|---|
| 108 | * @private
|
|---|
| 109 | */
|
|---|
| 110 | const createAnalyticsJsRoute = (cacheName) => {
|
|---|
| 111 | const match = ({ url }) => url.hostname === GOOGLE_ANALYTICS_HOST &&
|
|---|
| 112 | url.pathname === ANALYTICS_JS_PATH;
|
|---|
| 113 | const handler = new NetworkFirst({ cacheName });
|
|---|
| 114 | return new Route(match, handler, 'GET');
|
|---|
| 115 | };
|
|---|
| 116 | /**
|
|---|
| 117 | * Creates a route with a network first strategy for the gtag.js script.
|
|---|
| 118 | *
|
|---|
| 119 | * @param {string} cacheName
|
|---|
| 120 | * @return {Route} The created route.
|
|---|
| 121 | *
|
|---|
| 122 | * @private
|
|---|
| 123 | */
|
|---|
| 124 | const createGtagJsRoute = (cacheName) => {
|
|---|
| 125 | const match = ({ url }) => url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH;
|
|---|
| 126 | const handler = new NetworkFirst({ cacheName });
|
|---|
| 127 | return new Route(match, handler, 'GET');
|
|---|
| 128 | };
|
|---|
| 129 | /**
|
|---|
| 130 | * Creates a route with a network first strategy for the gtm.js script.
|
|---|
| 131 | *
|
|---|
| 132 | * @param {string} cacheName
|
|---|
| 133 | * @return {Route} The created route.
|
|---|
| 134 | *
|
|---|
| 135 | * @private
|
|---|
| 136 | */
|
|---|
| 137 | const createGtmJsRoute = (cacheName) => {
|
|---|
| 138 | const match = ({ url }) => url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH;
|
|---|
| 139 | const handler = new NetworkFirst({ cacheName });
|
|---|
| 140 | return new Route(match, handler, 'GET');
|
|---|
| 141 | };
|
|---|
| 142 | /**
|
|---|
| 143 | * @param {Object=} [options]
|
|---|
| 144 | * @param {Object} [options.cacheName] The cache name to store and retrieve
|
|---|
| 145 | * analytics.js. Defaults to the cache names provided by `workbox-core`.
|
|---|
| 146 | * @param {Object} [options.parameterOverrides]
|
|---|
| 147 | * [Measurement Protocol parameters](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters),
|
|---|
| 148 | * expressed as key/value pairs, to be added to replayed Google Analytics
|
|---|
| 149 | * requests. This can be used to, e.g., set a custom dimension indicating
|
|---|
| 150 | * that the request was replayed.
|
|---|
| 151 | * @param {Function} [options.hitFilter] A function that allows you to modify
|
|---|
| 152 | * the hit parameters prior to replaying
|
|---|
| 153 | * the hit. The function is invoked with the original hit's URLSearchParams
|
|---|
| 154 | * object as its only argument.
|
|---|
| 155 | *
|
|---|
| 156 | * @memberof workbox-google-analytics
|
|---|
| 157 | */
|
|---|
| 158 | const initialize = (options = {}) => {
|
|---|
| 159 | const cacheName = cacheNames.getGoogleAnalyticsName(options.cacheName);
|
|---|
| 160 | const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
|
|---|
| 161 | maxRetentionTime: MAX_RETENTION_TIME,
|
|---|
| 162 | onSync: createOnSyncCallback(options),
|
|---|
| 163 | });
|
|---|
| 164 | const routes = [
|
|---|
| 165 | createGtmJsRoute(cacheName),
|
|---|
| 166 | createAnalyticsJsRoute(cacheName),
|
|---|
| 167 | createGtagJsRoute(cacheName),
|
|---|
| 168 | ...createCollectRoutes(bgSyncPlugin),
|
|---|
| 169 | ];
|
|---|
| 170 | const router = new Router();
|
|---|
| 171 | for (const route of routes) {
|
|---|
| 172 | router.registerRoute(route);
|
|---|
| 173 | }
|
|---|
| 174 | router.addFetchListener();
|
|---|
| 175 | };
|
|---|
| 176 | export { initialize };
|
|---|