This repository has been archived by the owner on Dec 5, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #47 from jeffposnick/offline-analytics
Add in support for queueing and retrying failed Google Analytics pings.
- Loading branch information
Showing
4 changed files
with
280 additions
and
22 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// TODO (jeffposnick): Reach out to inexorabletash (Joshua Bell) about making this into a package | ||
// that we can install in a standard location and add to the dependencies. | ||
importScripts('scripts/third_party/simpledb_polyfill.js'); | ||
|
||
var DB_NAME = 'shed-offline-analytics'; | ||
var EXPIRATION_TIME_DELTA = 86400000; // One day, in milliseconds. | ||
|
||
function replayQueuedRequests() { | ||
simpleDB.open(DB_NAME).then(function(db) { | ||
db.forEach(function(url) { | ||
db.get(url).then(function(originalTimestamp) { | ||
var timeDelta = Date.now() - originalTimestamp; | ||
// See https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#qt | ||
var replayUrl = url + '&qt=' + timeDelta; | ||
|
||
console.log('About to replay:', replayUrl); | ||
fetch(replayUrl).then(function(response) { | ||
if (response.status >= 500) { | ||
// This will cause the promise to reject, triggering the .catch() function. | ||
return Response.error(); | ||
} | ||
|
||
console.log('Replay succeeded:', replayUrl); | ||
db.delete(url); | ||
}).catch(function(error) { | ||
if (timeDelta > EXPIRATION_TIME_DELTA) { | ||
// After a while, Google Analytics will no longer accept an old ping with a qt= | ||
// parameter. The advertised time is ~4 hours, but we'll attempt to resend up to 24 | ||
// hours. This logic also prevents the requests from being queued indefinitely. | ||
console.error('Replay failed, but the original request is too old to retry any further. Error:', error); | ||
db.delete(url); | ||
} else { | ||
console.error('Replay failed, and will be retried the next time the service worker starts. Error:', error); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
function queueFailedRequest(request) { | ||
console.log('Queueing failed request:', request); | ||
|
||
simpleDB.open(DB_NAME).then(function(db) { | ||
db.set(request.url, Date.now()); | ||
}); | ||
} | ||
|
||
function handleAnalyticsCollectionRequest(request) { | ||
return fetch(request).then(function(response) { | ||
if (response.status >= 500) { | ||
// This will cause the promise to reject, triggering the .catch() function. | ||
// It will also result in a generic HTTP error being returned to the controlled page. | ||
return Response.error(); | ||
} else { | ||
return response; | ||
} | ||
}).catch(function() { | ||
queueFailedRequest(request); | ||
}); | ||
} | ||
|
||
// TODO (jeffposnick): Is there any way to use wildcards for the protocols and domains? | ||
// TODO (jeffposnick): See if there's a way to avoid :ignored. See https://github.com/wibblymat/shed/issues/17 | ||
shed.router.get('http://www.google-analytics.com/collect?:ignored', handleAnalyticsCollectionRequest); | ||
shed.router.get('https://www.google-analytics.com/collect?:ignored', handleAnalyticsCollectionRequest); | ||
shed.router.get('https://ssl.google-analytics.com/collect?:ignored', handleAnalyticsCollectionRequest); | ||
|
||
shed.router.get('http://www.google-analytics.com/analytics.js', shed.networkFirst); | ||
shed.router.get('https://www.google-analytics.com/analytics.js', shed.networkFirst); | ||
|
||
replayQueuedRequests(); |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// From https://gist.github.com/inexorabletash/c8069c042b734519680c (Joshua Bell) | ||
|
||
(function(global) { | ||
var SECRET = Object.create(null); | ||
var DB_PREFIX = '$SimpleDB$'; | ||
var STORE = 'store'; | ||
|
||
function SimpleDBFactory(secret) { | ||
if (secret !== SECRET) throw TypeError('Invalid constructor'); | ||
} | ||
SimpleDBFactory.prototype = { | ||
open: function(name) { | ||
return new Promise(function(resolve, reject) { | ||
var request = indexedDB.open(DB_PREFIX + name); | ||
request.onupgradeneeded = function() { | ||
var db = request.result; | ||
db.createObjectStore(STORE); | ||
}; | ||
request.onsuccess = function() { | ||
var db = request.result; | ||
resolve(new SimpleDB(SECRET, name, db)); | ||
}; | ||
request.onerror = function() { | ||
reject(request.error); | ||
}; | ||
}); | ||
}, | ||
delete: function(name) { | ||
return new Promise(function(resolve, reject) { | ||
var request = indexedDB.deleteDatabase(DB_PREFIX + name); | ||
request.onsuccess = function() { | ||
resolve(undefined); | ||
}; | ||
request.onerror = function() { | ||
reject(request.error); | ||
}; | ||
}); | ||
} | ||
}; | ||
|
||
function SimpleDB(secret, name, db) { | ||
if (secret !== SECRET) throw TypeError('Invalid constructor'); | ||
this._name = name; | ||
this._db = db; | ||
} | ||
SimpleDB.cmp = indexedDB.cmp; | ||
SimpleDB.prototype = { | ||
get name() { | ||
return this._name; | ||
}, | ||
get: function(key) { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
var req = store.get(key); | ||
// NOTE: Could also use req.onsuccess/onerror | ||
tx.oncomplete = function() { resolve(req.result); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
}, | ||
set: function(key, value) { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
var req = store.put(value, key); | ||
tx.oncomplete = function() { resolve(undefined); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
}, | ||
delete: function(key) { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
var req = store.delete(key); | ||
tx.oncomplete = function() { resolve(undefined); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
}, | ||
clear: function() { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
var request = store.clear(); | ||
tx.oncomplete = function() { resolve(undefined); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
}, | ||
forEach: function(callback, options) { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
options = options || {}; | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
var request = store.openCursor( | ||
options.range, | ||
options.direction === 'reverse' ? 'prev' : 'next'); | ||
request.onsuccess = function() { | ||
var cursor = request.result; | ||
if (!cursor) return; | ||
try { | ||
var terminate = callback(cursor.key, cursor.value); | ||
if (!terminate) cursor.continue(); | ||
} catch (ex) { | ||
tx.abort(); // ??? | ||
} | ||
}; | ||
tx.oncomplete = function() { resolve(undefined); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
}, | ||
getMany: function(keys) { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
var results = []; | ||
for (var key of keys) { | ||
store.get(key).onsuccess(function(result) { | ||
results.push(result); | ||
}); | ||
} | ||
tx.oncomplete = function() { resolve(results); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
}, | ||
setMany: function(entries) { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
for (var entry of entries) { | ||
store.put(entry.value, entry.key); | ||
} | ||
tx.oncomplete = function() { resolve(undefined); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
}, | ||
deleteMany: function(keys) { | ||
var that = this; | ||
return new Promise(function(resolve, reject) { | ||
var tx = that._db.transaction(STORE, 'readwrite'); | ||
var store = tx.objectStore(STORE); | ||
for (var key of keys) | ||
store.delete(key); | ||
tx.oncomplete = function() { resolve(undefined); }; | ||
tx.onabort = function() { reject(tx.error); }; | ||
}); | ||
} | ||
}; | ||
|
||
global.simpleDB = new SimpleDBFactory(SECRET); | ||
global.SimpleDBKeyRange = IDBKeyRange; | ||
}(self)); |
This file contains 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