diff --git a/packages/browser/package.json b/packages/browser/package.json index a8e5bf864bbb..7ac8306756c5 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -17,8 +17,10 @@ }, "dependencies": { "@sentry/core": "5.20.1", + "@sentry/minimal": "5.20.1", "@sentry/types": "5.20.1", "@sentry/utils": "5.20.1", + "localforage": "^1.8.1", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/packages/browser/src/integrations/index.ts b/packages/browser/src/integrations/index.ts index 21a076b636f8..9afd53c80f2c 100644 --- a/packages/browser/src/integrations/index.ts +++ b/packages/browser/src/integrations/index.ts @@ -3,3 +3,4 @@ export { TryCatch } from './trycatch'; export { Breadcrumbs } from './breadcrumbs'; export { LinkedErrors } from './linkederrors'; export { UserAgent } from './useragent'; +export { Offline } from './offline'; diff --git a/packages/browser/src/integrations/offline.ts b/packages/browser/src/integrations/offline.ts new file mode 100644 index 000000000000..5f490886416c --- /dev/null +++ b/packages/browser/src/integrations/offline.ts @@ -0,0 +1,100 @@ +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; +import { captureEvent } from '@sentry/minimal'; +import { Event, Integration } from '@sentry/types'; +import { getGlobalObject, uuid4 } from '@sentry/utils'; +// @ts-ignore +import localforage = require('localforage'); + +/** + * cache offline errors and send when connected + */ +export class Offline implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Offline'; + + /** + * @inheritDoc + */ + public readonly name: string = Offline.id; + + /** + * event cache + */ + public offlineEventStore: LocalForage; + + /** + * @inheritDoc + */ + public constructor() { + this.offlineEventStore = localforage.createInstance({ + name: 'sentry/offlineEventStore', + }); + + getGlobalObject().addEventListener('online', () => { + this._sendEvents().catch(() => { + // todo: handle localforage error + }); + }); + } + + /** + * @inheritDoc + */ + public setupOnce(): void { + addGlobalEventProcessor(async (event: Event) => { + if (getCurrentHub().getIntegration(Offline)) { + const global = getGlobalObject(); + + // cache if we are positively offline + if ('navigator' in global && 'onLine' in global.navigator && !global.navigator.onLine) { + try { + await this._cacheEvent(event); + } catch (error) { + // todo: handle localforage error + } + + // return null on success or failure, because being offline will still result in an error + return null; + } + } + + return event; + }); + } + + /** + * cache an event to send later + * @param event an event + */ + private async _cacheEvent(event: Event): Promise { + return this.offlineEventStore.setItem(uuid4(), event); + } + + /** + * purge event from cache + */ + private async _purgeEvent(cacheKey: string): Promise { + return this.offlineEventStore.removeItem(cacheKey); + } + + /** + * send all events + */ + private async _sendEvents(): Promise { + return this.offlineEventStore.iterate((event: any, cacheKey: string, _index: number) => { + try { + const newEventId = captureEvent(event); + + if (newEventId) { + this._purgeEvent(cacheKey) + .then(_ => _) + .catch(_ => _); + } + } catch (error) { + // handle JSON.parse error + } + }); + } +} diff --git a/yarn.lock b/yarn.lock index 396297e1e022..ba6aa67cf9f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1939,11 +1939,32 @@ after@0.8.2: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= -agent-base@4, agent-base@5, agent-base@6, agent-base@^4.3.0, agent-base@~4.2.1: +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +agent-base@5: version "5.1.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== +agent-base@6: + version "6.0.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" + integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== + dependencies: + debug "4" + +agent-base@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + agentkeepalive@^3.4.1: version "3.5.2" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" @@ -5114,6 +5135,18 @@ es6-object-assign@^1.1.0: resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escalade@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" @@ -6528,6 +6561,11 @@ ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" @@ -8106,6 +8144,13 @@ libnpmpublish@^1.1.1: semver "^5.5.1" ssri "^6.0.1" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + dependencies: + immediate "~3.0.5" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -8155,6 +8200,13 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +localforage@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.8.1.tgz#f6c0a24b41ab33b10e4dc84342dd696f6f3e3433" + integrity sha512-azSSJJfc7h4bVpi0PGi+SmLQKJl2/8NErI+LhJsrORNikMZnhaQ7rv9fHj+ofwgSHrKRlsDCL/639a6nECIKuQ== + dependencies: + lie "3.1.1" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"