Last active
May 25, 2017 17:29
-
-
Save steveworkman/90936242b21c7b85a43525fd911c1242 to your computer and use it in GitHub Desktop.
Attempt to turn the offline-analytics example into a re-usable bit of code that is also testable. Each function now uses promises to keep state
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// IndexedDB properties | |
var idbDatabase; | |
const IDB_VERSION = 1, | |
STOP_RETRYING_AFTER = (1000 * 60 * 60 * 24), // One day, in milliseconds. | |
STORE_NAME = 'urls', | |
IDB_NAME = 'offline-analytics'; | |
// These URLs should always be fetched from the server, or page views don't count! | |
const analyticsDomains = [ | |
{ host: 'omniture.adobe.com', pathStart: '/b/ss'}, // Omniture | |
{ host: 'www.google-analytics.com', pathStart: '/collect' }, // Google Analytics | |
{ host: 'c.go-mpulse.net', pathStart: '/boomerang' } // mPulse | |
]; | |
// This code is pretty much word-for-word from https://googlechrome.github.io/samples/service-worker/offline-analytics/service-worker.js | |
// I've added promises to it as well, because it's not bullet-proof in the sample form | |
// This is basic boilerplate for interacting with IndexedDB. Adapted from | |
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB | |
// However, once I started doing unit tests I discovered a race condition, and the database wasn't always open | |
// So, this is now done using promises. | |
function openDatabase(storeName=STORE_NAME) { | |
return new Promise((resolve, reject) => { | |
var indexedDBOpenRequest = indexedDB.open(IDB_NAME, IDB_VERSION); | |
// This top-level error handler will be invoked any time there's an IndexedDB-related error. | |
indexedDBOpenRequest.onerror = function(error) { | |
reject(error); | |
}; | |
// This should only execute if there's a need to create a new database for the given IDB_VERSION. | |
indexedDBOpenRequest.onupgradeneeded = function() { | |
this.result.createObjectStore(storeName, {keyPath: 'url'}); | |
}; | |
// This will execute each time the database is opened. | |
indexedDBOpenRequest.onsuccess = function() { | |
idbDatabase = this.result; | |
resolve(idbDatabase); | |
}; | |
}); | |
} | |
// Runs once the service worker is brought back to life | |
function openDatabaseAndReplayRequests() { | |
openDatabase() | |
.then(replayAnalyticsRequests) | |
.catch(error => console.error(error)); | |
} | |
// Helper method to get the object store that we care about. | |
// This cannot be written with browser-native promises and work across browsers | |
// because only Chrome uses micro-transactions for promise events | |
// All other browsers make use of the event loop, and because of that, transactions time out when | |
// they resolve. So, this is an ES5 callback | |
function getObjectStore(storeName, mode, callback) { | |
if (idbDatabase) { | |
callback(null, idbDatabase.transaction(storeName, mode).objectStore(storeName)); | |
} else { | |
// database isn't open yet | |
openDatabase(storeName) | |
.then(() => { | |
callback(null, idbDatabase.transaction(storeName, mode).objectStore(storeName)); | |
}).catch((error) => { callback(error);}); | |
} | |
} | |
// Tried to replay the analytics requests | |
function replayAnalyticsRequests() { | |
var savedRequests = []; | |
getObjectStore(STORE_NAME, 'readonly', function(err, store) { | |
store.openCursor().onsuccess = function(event) { | |
// See https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#Using_a_cursor | |
var cursor = event.target.result; | |
if (cursor) { | |
// Keep moving the cursor forward and collecting saved requests. | |
savedRequests.push(cursor.value); | |
cursor.continue(); | |
} else { | |
// At this point, we have all the saved requests. | |
console.log(`About to replay ${savedRequests.length} saved Omniture requests`); | |
savedRequests.forEach(function(savedRequest) { | |
var queueTime = Date.now() - savedRequest.timestamp; | |
console.log(`Queue time: ${queueTime}`); | |
if (queueTime > STOP_RETRYING_AFTER) { | |
getObjectStore(STORE_NAME, 'readwrite', function(retryErr, rwstore) { if (retryErr) return; rwstore.delete(savedRequest.url); }); | |
console.log(`Request has been queued for ${queueTime} milliseconds. No longer attempting to replay.`); | |
} else { | |
// The qt= URL parameter specifies the time delta in between right now, and when the | |
// /collect request was initially intended to be sent. See | |
// https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#qt | |
var requestUrl = savedRequest.url + '&qt=' + queueTime; | |
console.log('Replaying', requestUrl); | |
fetch(requestUrl).then(function(response) { | |
if (response.status < 400) { | |
// If sending the /collect request was successful, then remove it from the IndexedDB. | |
getObjectStore(STORE_NAME, 'readwrite', function(replayErr, rwstore) { if (replayErr) return; rwstore.delete(savedRequest.url); }); | |
console.log(`Replayed ${savedRequest.url} successfully.`); | |
} else { | |
// This will be triggered if, e.g., Google Analytics returns a HTTP 50x response. | |
// The request will be replayed the next time the service worker starts up. | |
console.error(' Replaying failed:', response); | |
} | |
}).catch(function(error) { | |
// This will be triggered if the network is still down. The request will be replayed again | |
// the next time the service worker starts up. | |
console.error(' Replaying failed:', error); | |
}); | |
} | |
}); | |
} | |
}; | |
}); | |
} | |
// Open the IndexedDB and check for requests to replay each time the service worker starts up. | |
// Since the service worker is terminated fairly frequently, it should start up again for most | |
// page navigations. It also might start up if it's used in a background sync or a push | |
// notification context. | |
openDatabaseAndReplayRequests(); | |
// Checks a URL to see if it's a request to one of our analytics providers | |
// If it is, then save it to the IDB | |
function checkForAnalyticsRequest(requestUrl) { | |
// Construct a URL object (https://developer.mozilla.org/en-US/docs/Web/API/URL.URL) | |
// to make it easier to check the various components without dealing with string parsing. | |
var url = new URL(requestUrl); | |
if (analyticsDomains.some(fetchDomain => url.hostname === fetchDomain.host && url.pathname.startsWith(fetchDomain.pathStart))) { | |
console.log(`Storing ${requestUrl} request in IndexedDB to be replayed later.`); | |
saveAnalyticsRequest(requestUrl); | |
} | |
} | |
// Saves a request to the IDB | |
function saveAnalyticsRequest(requestUrl) { | |
getObjectStore(STORE_NAME, 'readwrite', function(err, store) { | |
store.add({ | |
url: requestUrl, | |
timestamp: Date.now() | |
}); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment