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