diff --git a/packages/integrations/rollup.bundle.config.mjs b/packages/integrations/rollup.bundle.config.mjs index 366585b8abe3..4ad9d857b496 100644 --- a/packages/integrations/rollup.bundle.config.mjs +++ b/packages/integrations/rollup.bundle.config.mjs @@ -1,6 +1,4 @@ -import commonjs from '@rollup/plugin-commonjs'; - -import { insertAt, makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; +import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; const builds = []; @@ -15,10 +13,6 @@ const baseBundleConfig = makeBaseBundleConfig({ outputFileBase: ({ name: entrypoint }) => `bundles/${entrypoint}${jsVersion === 'es5' ? '.es5' : ''}`, }); -// TODO We only need `commonjs` for localforage (used in the offline plugin). Once that's fixed, this can come out. -// CommonJS plugin docs: https://github.com/rollup/plugins/tree/master/packages/commonjs -baseBundleConfig.plugins = insertAt(baseBundleConfig.plugins, -2, commonjs()); - // this makes non-minified, minified, and minified-with-debug-logging versions of each bundle builds.push(...makeBundleConfigVariants(baseBundleConfig)); diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index 445e10fa463e..a73b14257ed7 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -3,7 +3,6 @@ export { CaptureConsole, captureConsoleIntegration } from './captureconsole'; export { Debug, debugIntegration } from './debug'; export { Dedupe, dedupeIntegration } from './dedupe'; export { ExtraErrorData, extraErrorDataIntegration } from './extraerrordata'; -export { Offline } from './offline'; export { ReportingObserver, reportingObserverIntegration } from './reportingobserver'; export { RewriteFrames, rewriteFramesIntegration } from './rewriteframes'; export { SessionTiming, sessionTimingIntegration } from './sessiontiming'; diff --git a/packages/integrations/src/offline.ts b/packages/integrations/src/offline.ts deleted file mode 100644 index 67a9df76eca7..000000000000 --- a/packages/integrations/src/offline.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable deprecation/deprecation */ -import type { Event, EventProcessor, Hub, Integration } from '@sentry/types'; -import { GLOBAL_OBJ, logger, normalize, uuid4 } from '@sentry/utils'; -import localForage from 'localforage'; - -import { DEBUG_BUILD } from './debug-build'; - -const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; - -type LocalForage = { - setItem(key: string, value: T, callback?: (err: any, value: T) => void): Promise; - iterate( - iteratee: (value: T, key: string, iterationNumber: number) => U, - callback?: (err: any, result: U) => void, - ): Promise; - removeItem(key: string, callback?: (err: any) => void): Promise; - length(): Promise; -}; - -export type Item = { key: string; value: Event }; - -/** - * cache offline errors and send when connected - * @deprecated The offline integration has been deprecated in favor of the offline transport wrapper. - * - * http://docs.sentry.io/platforms/javascript/configuration/transports/#offline-caching - */ -export class Offline implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Offline'; - - /** - * @inheritDoc - */ - public readonly name: string; - - /** - * the current hub instance - */ - public hub?: Hub; - - /** - * maximum number of events to store while offline - */ - public maxStoredEvents: number; - - /** - * event cache - */ - public offlineEventStore: LocalForage; - - /** - * @inheritDoc - */ - public constructor(options: { maxStoredEvents?: number } = {}) { - this.name = Offline.id; - - this.maxStoredEvents = options.maxStoredEvents || 30; // set a reasonable default - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this.offlineEventStore = localForage.createInstance({ - name: 'sentry/offlineEventStore', - }); - } - - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - this.hub = getCurrentHub(); - - if ('addEventListener' in WINDOW) { - WINDOW.addEventListener('online', () => { - void this._sendEvents().catch(() => { - DEBUG_BUILD && logger.warn('could not send cached events'); - }); - }); - } - - const eventProcessor: EventProcessor = event => { - // eslint-disable-next-line deprecation/deprecation - if (this.hub && this.hub.getIntegration(Offline)) { - // cache if we are positively offline - if ('navigator' in WINDOW && 'onLine' in WINDOW.navigator && !WINDOW.navigator.onLine) { - DEBUG_BUILD && logger.log('Event dropped due to being a offline - caching instead'); - - void this._cacheEvent(event) - .then((_event: Event): Promise => this._enforceMaxEvents()) - .catch((_error): void => { - DEBUG_BUILD && logger.warn('could not cache event while offline'); - }); - - // return null on success or failure, because being offline will still result in an error - return null; - } - } - - return event; - }; - - eventProcessor.id = this.name; - addGlobalEventProcessor(eventProcessor); - - // if online now, send any events stored in a previous offline session - if ('navigator' in WINDOW && 'onLine' in WINDOW.navigator && WINDOW.navigator.onLine) { - void this._sendEvents().catch(() => { - DEBUG_BUILD && logger.warn('could not send cached events'); - }); - } - } - - /** - * cache an event to send later - * @param event an event - */ - private async _cacheEvent(event: Event): Promise { - return this.offlineEventStore.setItem(uuid4(), normalize(event)); - } - - /** - * purge excess events if necessary - */ - private async _enforceMaxEvents(): Promise { - const events: Array<{ event: Event; cacheKey: string }> = []; - - return this.offlineEventStore - .iterate((event: Event, cacheKey: string, _index: number): void => { - // aggregate events - events.push({ cacheKey, event }); - }) - .then( - (): Promise => - // this promise resolves when the iteration is finished - this._purgeEvents( - // purge all events past maxStoredEvents in reverse chronological order - events - .sort((a, b) => (b.event.timestamp || 0) - (a.event.timestamp || 0)) - .slice(this.maxStoredEvents < events.length ? this.maxStoredEvents : events.length) - .map(event => event.cacheKey), - ), - ) - .catch((_error): void => { - DEBUG_BUILD && logger.warn('could not enforce max events'); - }); - } - - /** - * purge event from cache - */ - private async _purgeEvent(cacheKey: string): Promise { - return this.offlineEventStore.removeItem(cacheKey); - } - - /** - * purge events from cache - */ - private async _purgeEvents(cacheKeys: string[]): Promise { - // trail with .then to ensure the return type as void and not void|void[] - return Promise.all(cacheKeys.map(cacheKey => this._purgeEvent(cacheKey))).then(); - } - - /** - * send all events - */ - private async _sendEvents(): Promise { - return this.offlineEventStore.iterate((event: Event, cacheKey: string, _index: number): void => { - if (this.hub) { - this.hub.captureEvent(event); - - void this._purgeEvent(cacheKey).catch((_error): void => { - DEBUG_BUILD && logger.warn('could not purge event from cache'); - }); - } else { - DEBUG_BUILD && logger.warn('no hub found - could not send cached event'); - } - }); - } -} diff --git a/packages/integrations/test/offline.test.ts b/packages/integrations/test/offline.test.ts deleted file mode 100644 index 1dac96bc1e53..000000000000 --- a/packages/integrations/test/offline.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/* eslint-disable deprecation/deprecation */ -import { WINDOW } from '@sentry/browser'; -import type { Event, EventProcessor, Hub, Integration, IntegrationClass } from '@sentry/types'; - -import type { Item } from '../src/offline'; -import { Offline } from '../src/offline'; - -// mock localforage methods -jest.mock('localforage', () => ({ - createInstance(_options: { name: string }): any { - let items: Item[] = []; - - return { - async getItem(key: string): Promise { - return items.find(item => item.key === key); - }, - async iterate(callback: (event: Event, key: string, index: number) => void): Promise { - items.forEach((item, index) => { - callback(item.value, item.key, index); - }); - }, - async length(): Promise { - return items.length; - }, - async removeItem(key: string): Promise { - items = items.filter(item => item.key !== key); - }, - async setItem(key: string, value: Event): Promise { - items.push({ - key, - value, - }); - }, - }; - }, -})); - -let integration: Offline; - -// We need to mock the WINDOW object so we can modify 'navigator.online' which is readonly -jest.mock('@sentry/utils', () => { - const originalModule = jest.requireActual('@sentry/utils'); - - return { - ...originalModule, - get GLOBAL_OBJ() { - return { - addEventListener: (_windowEvent: any, callback: any) => { - eventListeners.push(callback); - }, - navigator: { - onLine: false, - }, - }; - }, - }; -}); - -describe('Offline', () => { - describe('when app is online', () => { - beforeEach(() => { - (WINDOW.navigator as any).onLine = true; - - initIntegration(); - }); - - it('does not store events in offline store', async () => { - setupOnce(); - processEvents(); - - expect(await integration.offlineEventStore.length()).toEqual(0); - }); - - describe('when there are already events in the cache from a previous offline session', () => { - beforeEach(async () => { - const event = { message: 'previous event' }; - - await integration.offlineEventStore.setItem('previous', event); - }); - - it('sends stored events', async () => { - expect(await integration.offlineEventStore.length()).toEqual(1); - - setupOnce(); - processEvents(); - - expect(await integration.offlineEventStore.length()).toEqual(0); - }); - }); - }); - - describe('when app is offline', () => { - beforeEach(() => { - (WINDOW.navigator as any).onLine = false; - }); - - it('stores events in offline store', async () => { - initIntegration(); - setupOnce(); - prepopulateEvents(1); - processEvents(); - - expect(await integration.offlineEventStore.length()).toEqual(1); - }); - - it('enforces a default of 30 maxStoredEvents', done => { - initIntegration(); - setupOnce(); - prepopulateEvents(50); - processEvents(); - - setImmediate(async () => { - // allow background promises to finish resolving - expect(await integration.offlineEventStore.length()).toEqual(30); - done(); - }); - }); - - it('does not purge events when below the maxStoredEvents threshold', done => { - initIntegration(); - setupOnce(); - prepopulateEvents(5); - processEvents(); - - setImmediate(async () => { - // allow background promises to finish resolving - expect(await integration.offlineEventStore.length()).toEqual(5); - done(); - }); - }); - - describe('when maxStoredEvents is supplied', () => { - it('respects the configuration', done => { - initIntegration({ maxStoredEvents: 5 }); - setupOnce(); - prepopulateEvents(50); - processEvents(); - - setImmediate(async () => { - // allow background promises to finish resolving - expect(await integration.offlineEventStore.length()).toEqual(5); - done(); - }); - }); - }); - - describe('when connectivity is restored', () => { - it('sends stored events', async () => { - initIntegration(); - setupOnce(); - prepopulateEvents(1); - processEvents(); - processEventListeners(); - - expect(await integration.offlineEventStore.length()).toEqual(0); - }); - }); - }); -}); - -let eventListeners: any[]; -let eventProcessors: EventProcessor[]; -let events: Event[]; - -/** JSDoc */ -function addGlobalEventProcessor(callback: EventProcessor): void { - eventProcessors.push(callback); -} - -/** JSDoc */ -function getCurrentHub(): Hub { - return { - captureEvent(_event: Event): string { - return 'an-event-id'; - }, - getIntegration(_integration: IntegrationClass): T | null { - // pretend integration is enabled - return {} as T; - }, - } as Hub; -} - -/** JSDoc */ -function initIntegration(options: { maxStoredEvents?: number } = {}): void { - eventListeners = []; - eventProcessors = []; - events = []; - - integration = new Offline(options); -} - -/** JSDoc */ -function prepopulateEvents(count: number = 1): void { - for (let i = 0; i < count; i++) { - events.push({ - message: 'There was an error!', - timestamp: Date.now(), - }); - } -} - -/** JSDoc */ -function processEventListeners(): void { - eventListeners.forEach(listener => { - listener(); - }); -} - -/** JSDoc */ -function processEvents(): void { - eventProcessors.forEach(processor => { - events.forEach(event => { - processor(event, {}) as Event | null; - }); - }); -} - -/** JSDoc */ -function setupOnce(): void { - integration.setupOnce(addGlobalEventProcessor, getCurrentHub); -}