From 0011cc7f7d0b1c71ec169eff53b21c2e3223d3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Mon, 6 Nov 2023 14:46:36 +0100 Subject: [PATCH 1/4] ref(integrations): remove offline integration BREAKING CHANGE: Removes the already deprecated offline integration. --- .../integrations/rollup.bundle.config.mjs | 8 +- packages/integrations/src/index.ts | 1 - packages/integrations/src/offline.ts | 181 -------------- packages/integrations/test/offline.test.ts | 221 ------------------ 4 files changed, 1 insertion(+), 410 deletions(-) delete mode 100644 packages/integrations/src/offline.ts delete mode 100644 packages/integrations/test/offline.test.ts 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); -} From 783ccac3fea0eca4707d5ae034d0fd8bc4925918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Mon, 6 Nov 2023 14:58:40 +0100 Subject: [PATCH 2/4] ref(integrations): remove localforage --- packages/integrations/package.json | 3 +-- yarn.lock | 19 ------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/integrations/package.json b/packages/integrations/package.json index b016aaf1d9e8..46f3485228cd 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -31,8 +31,7 @@ "dependencies": { "@sentry/core": "7.100.0", "@sentry/types": "7.100.0", - "@sentry/utils": "7.100.0", - "localforage": "^1.8.1" + "@sentry/utils": "7.100.0" }, "devDependencies": { "@sentry/browser": "7.100.0", diff --git a/yarn.lock b/yarn.lock index ddf84ad0e320..9f5971f8838d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18176,11 +18176,6 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= - immutable@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" @@ -20653,13 +20648,6 @@ license-webpack-plugin@2.3.20: "@types/webpack-sources" "^0.1.5" webpack-sources "^1.2.0" -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" - lilconfig@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" @@ -20805,13 +20793,6 @@ local-pkg@^0.4.2: resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963" integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g== -localforage@^1.8.1: - version "1.9.0" - resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1" - integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g== - dependencies: - lie "3.1.1" - locate-character@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974" From 56576c486780ed1c8844885ec1824c9330a1f470 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 7 Feb 2024 15:04:29 +0000 Subject: [PATCH 3/4] Remove reserved mangling keyword related to localforage --- dev-packages/rollup-utils/plugins/bundlePlugins.mjs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index b0a1c806ef98..e2a96c9697de 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -116,12 +116,6 @@ export function makeTerserPlugin() { reserved: [ // ...except for `_experiments`, which we want to remain usable from the outside '_experiments', - // ...except for some localforage internals, which if we replaced them would break the localforage package - // with the error "Error: Custom driver not compliant": https://github.com/getsentry/sentry-javascript/issues/5527. - // Reference for which fields are affected: https://localforage.github.io/localForage/ (ctrl-f for "_") - '_driver', - '_initStorage', - '_support', // We want to keep some replay fields unmangled to enable integration tests to access them '_replay', '_canvas', From 3ccb6dbf4fef5fdd507f82623cb83ff6c30ac55e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 7 Feb 2024 15:42:11 +0000 Subject: [PATCH 4/4] Add note to migration docs --- MIGRATION.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index 626d95bb88cd..f975b13c0f4d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -6,6 +6,11 @@ In v7 we deprecated the `Severity` enum in favor of using the `SeverityLevel` ty enum. If you were using the `Severity` enum, you should replace it with the `SeverityLevel` type. See [below](#severity-severitylevel-and-severitylevels) for code snippet examples +## Removal of the `Offline` integration + +The `Offline` integration has been removed in favor of the offline transport wrapper: +http://docs.sentry.io/platforms/javascript/configuration/transports/#offline-caching + # Deprecations in 7.x You can use the **Experimental** [@sentry/migr8](https://www.npmjs.com/package/@sentry/migr8) to automatically update