diff --git a/packages/browser/package.json b/packages/browser/package.json index 3d286c2c9c64..5fc1c5337c92 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -19,7 +19,8 @@ "@sentry/core": "5.6.2", "@sentry/types": "5.6.1", "@sentry/utils": "5.6.1", - "tslib": "^1.9.3" + "tslib": "^1.9.3", + "localforage": "1.7.3" }, "devDependencies": { "@types/md5": "2.1.33", 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..8bc0ef7d2434 --- /dev/null +++ b/packages/browser/src/integrations/offline.ts @@ -0,0 +1,116 @@ +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; +import { Event, Integration } from '@sentry/types'; +import localforage from 'localforage'; + +import { captureEvent } from '../index'; + +/** + * store errors occuring offline and send them when online again + */ +export class Offline implements Integration { + /** + * @inheritDoc + */ + public readonly name: string = Offline.id; + + /** + * @inheritDoc + */ + public static id: string = 'Offline'; + + /** + * the key to store the offline event queue + */ + private readonly _storrageKey: string = 'offlineEventStore'; + + public offlineEventStore: LocalForage; + + /** + * @inheritDoc + */ + public setupOnce(): void { + addGlobalEventProcessor(async (event: Event) => { + const self = getCurrentHub().getIntegration(Offline); + if (self) { + if (navigator.onLine) { + return event; + } + await this._storeEvent(event); + return null; + // self._storeEvent(event); + } + return event; + }); + } + + public constructor() { + this.offlineEventStore = localforage.createInstance({ + name: 'sentryOfflineEventStore', + }); + window.addEventListener('online', () => { + this._drainQueue().catch(function(): void { + // TODO: handle rejected promise + }); + }); + } + + /** + * store an event + * @param event an event + */ + private async _storeEvent(event: Event): Promise { + const storrageKey = this._storrageKey; + const offlineEventStore = this.offlineEventStore; + const promise: Promise = new Promise(async function(resolve: () => void, reject: () => void): Promise { + let queue: Event[] = []; + const value = await offlineEventStore.getItem(storrageKey); + // .then(function(value: unknown): void { + // }) + // .catch(function(err: Error): void { + // console.log(err); + // }); + if (typeof value === 'string') { + queue = JSON.parse(value); + } + queue.push(event); + await offlineEventStore.setItem(storrageKey, JSON.stringify(queue)).catch(function(): void { + // reject promise because saving to the localForge store did not work + reject(); + }); + resolve(); + }); + return promise; + } + + /** + * capture all events in the queue + */ + private async _drainQueue(): Promise { + const storrageKey = this._storrageKey; + const offlineEventStore = this.offlineEventStore; + const promise: Promise = new Promise(async function(resolve: () => void, reject: () => void): Promise { + let queue: Event[] = []; + // get queue + const value = await offlineEventStore.getItem(storrageKey).catch(function(): void { + // could not get queue from localForge, TODO: how to handle error? + }); + // TODO: check if value in localForge can be converted with JSON.parse + if (typeof value === 'string') { + queue = JSON.parse(value); + } + await offlineEventStore.removeItem(storrageKey).catch(function(): void { + // could not remove queue from localForge + reject(); + }); + // process all events in the queue + while (queue.length > 0) { + const event = queue.pop(); + if (typeof event !== 'undefined') { + captureEvent(event); + } + } + resolve(); + }); + return promise; + } +} diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 5328a23a6425..e06e69ad63a6 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -4,7 +4,7 @@ import { getGlobalObject } from '@sentry/utils'; import { BrowserOptions } from './backend'; import { BrowserClient, ReportDialogOptions } from './client'; import { wrap as internalWrap } from './helpers'; -import { Breadcrumbs, GlobalHandlers, LinkedErrors, TryCatch, UserAgent } from './integrations'; +import { Breadcrumbs, GlobalHandlers, LinkedErrors, Offline, TryCatch, UserAgent } from './integrations'; export const defaultIntegrations = [ new CoreIntegrations.InboundFilters(), @@ -14,6 +14,7 @@ export const defaultIntegrations = [ new GlobalHandlers(), new LinkedErrors(), new UserAgent(), + new Offline(), ]; /** diff --git a/packages/browser/tsconfig.build.json b/packages/browser/tsconfig.build.json index a263a085c70a..f93bd2173a6d 100644 --- a/packages/browser/tsconfig.build.json +++ b/packages/browser/tsconfig.build.json @@ -2,7 +2,11 @@ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": ".", - "outDir": "dist" + "outDir": "./dist" }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "exclude": [ + "build/**/*", + "dist/**/*" + ] } diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json index 15c1a0af3c44..2fc2ab23c61b 100644 --- a/packages/browser/tsconfig.json +++ b/packages/browser/tsconfig.json @@ -4,6 +4,8 @@ "exclude": ["dist"], "compilerOptions": { "rootDir": ".", - "types": ["node", "mocha", "chai", "sinon"] + "types": ["node", "mocha", "chai", "sinon"], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true } } diff --git a/tsconfig.json b/tsconfig.json index 154bca11059f..e343cafc52d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,8 @@ "@sentry/*": ["*/src"], "raven-js": ["raven-js/src/singleton.js"], "raven-node": ["raven-node/lib/client.js"] - } + }, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true } } diff --git a/yarn.lock b/yarn.lock index 2044db0bac4b..847755dd6320 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5490,6 +5490,11 @@ ignore@^3.3.5: resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== +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" @@ -6940,6 +6945,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" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -6973,6 +6985,13 @@ loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" +localforage@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204" + integrity sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ== + 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"