From dc216492afb5b6033065d997fe4b65ec0658890a Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Fri, 15 Dec 2023 15:53:44 -0800 Subject: [PATCH 01/11] feat: adds support for setting offline mode --- .../analytics-browser/src/browser-client.ts | 10 +++++ packages/analytics-browser/src/config.ts | 2 + .../test/browser-client.test.ts | 22 +++++++++++ .../analytics-browser/test/config.test.ts | 4 ++ .../analytics-browser/test/helpers/mock.ts | 1 + packages/analytics-core/src/config.ts | 3 ++ .../analytics-core/src/plugins/destination.ts | 5 ++- packages/analytics-core/test/config.test.ts | 3 ++ .../test/plugins/destination.test.ts | 38 +++++++++++++++++++ packages/analytics-node/test/config.test.ts | 2 + .../test/config.test.ts | 4 ++ packages/analytics-types/src/config/core.ts | 1 + .../test/page-view-tracking.test.ts | 1 + .../test/web-attribution.test.ts | 1 + 14 files changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 8c7c2b3c0..91960d3ab 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -264,4 +264,14 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { return super.process(event); } + + setOffline(offline: boolean) { + const previousOffline = this.config.offline; + this.config.offline = offline; + + // flush when modes changes offline to online + if (previousOffline && !offline) { + this.flush(); + } + } } diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 74bd6e4de..04e05d2f8 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -60,6 +60,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public loggerProvider: ILogger = new Logger(), public logLevel: LogLevel = LogLevel.Warn, public minIdLength?: number, + public offline = false, optOut = false, public partnerId?: string, public plan?: Plan, @@ -236,6 +237,7 @@ export const useBrowserConfig = async ( options.loggerProvider, options.logLevel, options.minIdLength, + options.offline, optOut, options.partnerId, options.plan, diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 72c858bd3..0804222e0 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -837,4 +837,26 @@ describe('browser-client', () => { expect(result.code).toBe(0); }); }); + + describe('setOffline', () => { + test('should set offline to true', async () => { + await client.init(apiKey, { + defaultTracking: false, + offline: false, + }).promise; + + client.setOffline(true); + expect(client.config.offline).toBe(true); + }); + + test('should set offline to false', async () => { + await client.init(apiKey, { + defaultTracking: false, + offline: true, + }).promise; + + client.setOffline(false); + expect(client.config.offline).toBe(false); + }); + }); }); diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index a20df4227..625795cfb 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -58,6 +58,7 @@ describe('config', () => { loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, + offline: false, partnerId: undefined, plan: undefined, ingestionMetadata: undefined, @@ -113,6 +114,7 @@ describe('config', () => { loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, + offline: false, partnerId: undefined, plan: undefined, ingestionMetadata: undefined, @@ -165,6 +167,7 @@ describe('config', () => { upgrade: false, }, defaultTracking: true, + offline: true, }, new AmplitudeBrowser(), ); @@ -197,6 +200,7 @@ describe('config', () => { logLevel: 2, loggerProvider: logger, minIdLength: undefined, + offline: true, partnerId: 'partnerId', plan: { version: '0', diff --git a/packages/analytics-browser/test/helpers/mock.ts b/packages/analytics-browser/test/helpers/mock.ts index ba3dae512..e061b6866 100644 --- a/packages/analytics-browser/test/helpers/mock.ts +++ b/packages/analytics-browser/test/helpers/mock.ts @@ -37,6 +37,7 @@ export const createConfigurationMock = (options?: Partial) => { logLevel: LogLevel.Warn, loggerProvider: new Logger(), minIdLength: undefined, + offline: false, optOut: false, plan: undefined, ingestionMetadata: undefined, diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index bb6ee4081..2c586c78a 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -26,6 +26,7 @@ export const getDefaultConfig = () => ({ instanceName: '$default_instance', logLevel: LogLevel.Warn, loggerProvider: new Logger(), + offline: false, optOut: false, serverUrl: AMPLITUDE_SERVER_URL, serverZone: 'US' as ServerZoneType, @@ -41,6 +42,7 @@ export class Config implements IConfig { loggerProvider: ILogger; logLevel: LogLevel; minIdLength?: number; + offline: boolean; plan?: Plan; ingestionMetadata?: IngestionMetadata; serverUrl: string | undefined; @@ -69,6 +71,7 @@ export class Config implements IConfig { this.minIdLength = options.minIdLength; this.plan = options.plan; this.ingestionMetadata = options.ingestionMetadata; + this.offline = options.offline ?? defaultConfig.offline; this.optOut = options.optOut ?? defaultConfig.optOut; this.serverUrl = options.serverUrl; this.serverZone = options.serverZone || defaultConfig.serverZone; diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 78b4a0251..e8404c987 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -106,7 +106,10 @@ export class Destination implements DestinationPlugin { } schedule(timeout: number) { - if (this.scheduled) return; + if (this.scheduled || this.config.offline) { + return; + } + this.scheduled = setTimeout(() => { void this.flush(true).then(() => { if (this.queue.length > 0) { diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index c449071af..1626678ac 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -26,6 +26,7 @@ describe('config', () => { logLevel: LogLevel.Warn, loggerProvider: new Logger(), minIdLength: undefined, + offline: false, _optOut: false, // private for `optOut` getter/setter partnerId: undefined, plan: undefined, @@ -44,6 +45,7 @@ describe('config', () => { const config = new Config({ apiKey: API_KEY, logLevel: LogLevel.Verbose, + offline: true, optOut: true, plan: { version: '0' }, ingestionMetadata: { @@ -63,6 +65,7 @@ describe('config', () => { logLevel: LogLevel.Verbose, loggerProvider: new Logger(), minIdLength: undefined, + offline: true, _optOut: true, plan: { version: '0', diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 6ccab58f0..ecdd4c975 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -122,6 +122,10 @@ describe('destination', () => { timeout: 0, }, ]; + destination.config = { + ...destination.config, + offline: false, + }; const flush = jest .spyOn(destination, 'flush') .mockImplementationOnce(() => { @@ -140,6 +144,40 @@ describe('destination', () => { expect(flush).toHaveBeenCalledTimes(2); }); + test('should not schedule a flush', async () => { + const destination = new Destination(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (destination as any).scheduled = null; + destination.queue = [ + { + event: { event_type: 'event_type' }, + attempts: 0, + callback: () => undefined, + timeout: 0, + }, + ]; + destination.config = { + ...destination.config, + offline: true, + }; + const flush = jest + .spyOn(destination, 'flush') + .mockImplementationOnce(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (destination as any).scheduled = null; + return Promise.resolve(undefined); + }) + .mockReturnValueOnce(Promise.resolve(undefined)); + destination.schedule(0); + // exhause first setTimeout + jest.runAllTimers(); + // wait for next tick to call nested setTimeout + await Promise.resolve(); + // exhause nested setTimeout + jest.runAllTimers(); + expect(flush).toHaveBeenCalledTimes(0); + }); + test('should not schedule if one is already in progress', () => { const destination = new Destination(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access diff --git a/packages/analytics-node/test/config.test.ts b/packages/analytics-node/test/config.test.ts index ebe0e65f5..f4657ab9d 100644 --- a/packages/analytics-node/test/config.test.ts +++ b/packages/analytics-node/test/config.test.ts @@ -19,6 +19,7 @@ describe('config', () => { instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, + offline: false, _optOut: false, plan: undefined, ingestionMetadata: undefined, @@ -45,6 +46,7 @@ describe('config', () => { instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, + offline: false, _optOut: false, partnerId: undefined, plan: undefined, diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index cc2470b29..ef7a966b1 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -38,6 +38,7 @@ describe('config', () => { loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, + offline: false, _optOut: false, partnerId: undefined, plan: undefined, @@ -90,6 +91,7 @@ describe('config', () => { loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, + offline: false, _optOut: false, partnerId: undefined, plan: undefined, @@ -143,6 +145,7 @@ describe('config', () => { sessionTimeout: 1, cookieUpgrade: false, disableCookies: true, + offline: true, }); expect(config).toEqual({ apiKey: API_KEY, @@ -164,6 +167,7 @@ describe('config', () => { loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, + offline: true, _optOut: false, partnerId: 'partnerId', plan: { diff --git a/packages/analytics-types/src/config/core.ts b/packages/analytics-types/src/config/core.ts index 71c6b086d..5fe79ea4f 100644 --- a/packages/analytics-types/src/config/core.ts +++ b/packages/analytics-types/src/config/core.ts @@ -15,6 +15,7 @@ export interface Config { logLevel: LogLevel; loggerProvider: Logger; minIdLength?: number; + offline: boolean; optOut: boolean; plan?: Plan; ingestionMetadata?: IngestionMetadata; diff --git a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts index 9570fddfd..43fee605d 100644 --- a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts +++ b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts @@ -12,6 +12,7 @@ describe('pageViewTrackingPlugin', () => { flushQueueSize: 0, logLevel: LogLevel.None, loggerProvider: new Logger(), + offline: false, optOut: false, serverUrl: undefined, transportProvider: new FetchTransport(), diff --git a/packages/plugin-web-attribution-browser/test/web-attribution.test.ts b/packages/plugin-web-attribution-browser/test/web-attribution.test.ts index 6ba4cfc86..0b42c40fd 100644 --- a/packages/plugin-web-attribution-browser/test/web-attribution.test.ts +++ b/packages/plugin-web-attribution-browser/test/web-attribution.test.ts @@ -13,6 +13,7 @@ describe('webAttributionPlugin', () => { flushQueueSize: 0, logLevel: LogLevel.None, loggerProvider: new Logger(), + offline: false, optOut: false, serverUrl: undefined, transportProvider: new FetchTransport(), From 379ccc96be9f6367a5011e901a1d98c3245a9751 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Mon, 18 Dec 2023 11:32:24 -0800 Subject: [PATCH 02/11] feat: adds support for setting offline mode --- packages/analytics-browser/src/browser-client-factory.ts | 6 ++++++ packages/analytics-browser/test/browser-client.test.ts | 2 ++ packages/analytics-types/src/client/web-client.ts | 2 ++ 3 files changed, 10 insertions(+) diff --git a/packages/analytics-browser/src/browser-client-factory.ts b/packages/analytics-browser/src/browser-client-factory.ts index bf8c4744e..21d075e32 100644 --- a/packages/analytics-browser/src/browser-client-factory.ts +++ b/packages/analytics-browser/src/browser-client-factory.ts @@ -125,6 +125,12 @@ export const createInstance = (): BrowserClient => { getClientLogConfig(client), getClientStates(client, ['config']), ), + setOffline: debugWrapper( + client.setOffline.bind(client), + 'setOffline', + getClientLogConfig(client), + getClientStates(client, ['config']), + ), }; }; diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 0804222e0..b2bae05de 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -854,9 +854,11 @@ describe('browser-client', () => { defaultTracking: false, offline: true, }).promise; + const flush = jest.spyOn(client, 'flush'); client.setOffline(false); expect(client.config.offline).toBe(false); + expect(flush).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/analytics-types/src/client/web-client.ts b/packages/analytics-types/src/client/web-client.ts index f9e34ff33..c29b9ceb8 100644 --- a/packages/analytics-types/src/client/web-client.ts +++ b/packages/analytics-types/src/client/web-client.ts @@ -137,6 +137,8 @@ export interface BrowserClient extends Client { * ``` */ add(plugin: Plugin): AmplitudeReturn; + + setOffline(offline: boolean): void; } export interface ReactNativeClient extends Client { From 2e5f444cf77879cadb5869df9a37fb58b176c231 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Mon, 18 Dec 2023 11:39:14 -0800 Subject: [PATCH 03/11] feat: adds support for setting offline mode --- packages/analytics-browser/test/helpers/mock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/analytics-browser/test/helpers/mock.ts b/packages/analytics-browser/test/helpers/mock.ts index e061b6866..245286e7c 100644 --- a/packages/analytics-browser/test/helpers/mock.ts +++ b/packages/analytics-browser/test/helpers/mock.ts @@ -22,6 +22,7 @@ export const createAmplitudeMock = (): jest.MockedObject => ({ extendSession: jest.fn(), reset: jest.fn(), setTransport: jest.fn(), + setOffline: jest.fn(), }); export const createConfigurationMock = (options?: Partial) => { From f416d2c24a92746ac30d7cc2a60d8bbdefca0c4e Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 4 Jan 2024 16:21:22 -0800 Subject: [PATCH 04/11] feat: offline mode --- .../src/browser-client-factory.ts | 6 -- .../analytics-browser/src/browser-client.ts | 29 ++++--- .../src/plugins/network-checker-plugin.ts | 23 +++++ .../test/browser-client.test.ts | 86 +++++++++++++------ .../analytics-browser/test/helpers/mock.ts | 1 - .../analytics-core/src/plugins/destination.ts | 5 ++ .../test/plugins/destination.test.ts | 19 ++++ .../analytics-types/src/client/web-client.ts | 2 - 8 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 packages/analytics-browser/src/plugins/network-checker-plugin.ts diff --git a/packages/analytics-browser/src/browser-client-factory.ts b/packages/analytics-browser/src/browser-client-factory.ts index 21d075e32..bf8c4744e 100644 --- a/packages/analytics-browser/src/browser-client-factory.ts +++ b/packages/analytics-browser/src/browser-client-factory.ts @@ -125,12 +125,6 @@ export const createInstance = (): BrowserClient => { getClientLogConfig(client), getClientStates(client, ['config']), ), - setOffline: debugWrapper( - client.setOffline.bind(client), - 'setOffline', - getClientLogConfig(client), - getClientStates(client, ['config']), - ), }; }; diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 91960d3ab..fec3f8737 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -1,5 +1,6 @@ import { AmplitudeCore, Destination, Identify, returnWrapper, Revenue, UUID } from '@amplitude/analytics-core'; import { + getGlobalScope, getAnalyticsConnector, getAttributionTrackingConfig, getPageViewTrackingConfig, @@ -31,6 +32,7 @@ import { formInteractionTracking } from './plugins/form-interaction-tracking'; import { fileDownloadTracking } from './plugins/file-download-tracking'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_START_EVENT } from './constants'; import { detNotify } from './det-notification'; +import { NetworkCheckerPlugin } from './plugins/network-checker-plugin'; export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -86,6 +88,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Step 4: Install plugins // Do not track any events before this + await this.add(new NetworkCheckerPlugin()).promise; await this.add(new Destination()).promise; await this.add(new Context()).promise; await this.add(new IdentityEventSender()).promise; @@ -120,6 +123,22 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { connector.eventBridge.setEventReceiver((event) => { void this.track(event.eventType, event.eventProperties); }); + + // Step 8: Add network listeners for offline mode + const globalScope = getGlobalScope(); + if (globalScope) { + globalScope.addEventListener('online', () => { + this.config.offline = false; + // Flush immediately cause ERR_NETWORK_CHANGED + setTimeout(() => { + this.flush(); + }, this.config.flushIntervalMillis); + }); + + globalScope.addEventListener('offline', () => { + this.config.offline = true; + }); + } } getUserId() { @@ -264,14 +283,4 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { return super.process(event); } - - setOffline(offline: boolean) { - const previousOffline = this.config.offline; - this.config.offline = offline; - - // flush when modes changes offline to online - if (previousOffline && !offline) { - this.flush(); - } - } } diff --git a/packages/analytics-browser/src/plugins/network-checker-plugin.ts b/packages/analytics-browser/src/plugins/network-checker-plugin.ts new file mode 100644 index 000000000..efab8b424 --- /dev/null +++ b/packages/analytics-browser/src/plugins/network-checker-plugin.ts @@ -0,0 +1,23 @@ +import { BeforePlugin, Event } from '@amplitude/analytics-types'; +import { BrowserConfig } from 'src/config'; + +export class NetworkCheckerPlugin implements BeforePlugin { + name = '@amplitude/plugin-network-checker-browser'; + type = 'before' as const; + + // this.config is defined in setup() which will always be called first + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + config: BrowserConfig; + + setup(config: BrowserConfig): Promise { + this.config = config; + return Promise.resolve(undefined); + } + + async execute(context: Event): Promise { + console.log('NetworkCheckerPlugin execute, offline: ', !navigator.onLine); + this.config.offline = !navigator.onLine; + return context; + } +} diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index b2bae05de..538c18825 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -10,6 +10,7 @@ import { getCookieName, } from '@amplitude/analytics-client-common'; import * as SnippetHelper from '../src/utils/snippet-helper'; +import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; import * as fileDownloadTracking from '../src/plugins/file-download-tracking'; import * as formInteractionTracking from '../src/plugins/form-interaction-tracking'; import * as webAttributionPlugin from '@amplitude/plugin-web-attribution-browser'; @@ -261,6 +262,67 @@ describe('browser-client', () => { }).promise; expect(webAttributionPluginPlugin).toHaveBeenCalledTimes(1); }); + + test('should listen for network change to online', async () => { + jest.useFakeTimers(); + const addEventListenerMock = jest.spyOn(window, 'addEventListener'); + // const setTimeoutSpy = jest.spyOn(window, 'setTimeout'); + const flush = jest.spyOn(client, 'flush').mockReturnValue({ promise: Promise.resolve() }); + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + window.dispatchEvent(new Event('online')); + + expect(addEventListenerMock).toHaveBeenCalledWith('online', expect.any(Function)); + expect(client.config.offline).toBe(false); + // expect(setTimeoutSpy).toHaveBeenCalledTimes(1); + // expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), client.config.flushIntervalMillis); + + jest.advanceTimersByTime(client.config.flushIntervalMillis); + expect(flush).toHaveBeenCalledTimes(1); + + jest.useRealTimers(); + addEventListenerMock.mockRestore(); + // setTimeoutSpy.mockRestore(); + flush.mockRestore(); + }); + + test('should listen for network change to offline', async () => { + jest.useFakeTimers(); + const addEventListenerMock = jest.spyOn(window, 'addEventListener'); + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + expect(client.config.offline).toBe(false); + + window.dispatchEvent(new Event('offline')); + expect(addEventListenerMock).toHaveBeenCalledWith('offline', expect.any(Function)); + expect(client.config.offline).toBe(true); + + jest.useRealTimers(); + addEventListenerMock.mockRestore(); + }); + + test('should not support offline mode if global scope returns undefined', async () => { + const getGlobalScopeMock = jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValueOnce(undefined); + const addEventListenerMock = jest.spyOn(window, 'addEventListener'); + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + + window.dispatchEvent(new Event('online')); + expect(client.config.offline).toBe(false); + + client.config.offline = true; + window.dispatchEvent(new Event('offline')); + expect(client.config.offline).toBe(true); + + getGlobalScopeMock.mockRestore(); + addEventListenerMock.mockRestore(); + }); }); describe('getUserId', () => { @@ -837,28 +899,4 @@ describe('browser-client', () => { expect(result.code).toBe(0); }); }); - - describe('setOffline', () => { - test('should set offline to true', async () => { - await client.init(apiKey, { - defaultTracking: false, - offline: false, - }).promise; - - client.setOffline(true); - expect(client.config.offline).toBe(true); - }); - - test('should set offline to false', async () => { - await client.init(apiKey, { - defaultTracking: false, - offline: true, - }).promise; - const flush = jest.spyOn(client, 'flush'); - - client.setOffline(false); - expect(client.config.offline).toBe(false); - expect(flush).toHaveBeenCalledTimes(1); - }); - }); }); diff --git a/packages/analytics-browser/test/helpers/mock.ts b/packages/analytics-browser/test/helpers/mock.ts index 245286e7c..e061b6866 100644 --- a/packages/analytics-browser/test/helpers/mock.ts +++ b/packages/analytics-browser/test/helpers/mock.ts @@ -22,7 +22,6 @@ export const createAmplitudeMock = (): jest.MockedObject => ({ extendSession: jest.fn(), reset: jest.fn(), setTransport: jest.fn(), - setOffline: jest.fn(), }); export const createConfigurationMock = (options?: Partial) => { diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index e8404c987..d5d0a5df6 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -120,6 +120,11 @@ export class Destination implements DestinationPlugin { } async flush(useRetry = false) { + // Skip flush if offline + if (this.config.offline) { + return; + } + const list: Context[] = []; const later: Context[] = []; this.queue.forEach((context) => (context.timeout === 0 ? list.push(context) : later.push(context))); diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index ecdd4c975..8f3a43e91 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -189,6 +189,25 @@ describe('destination', () => { }); describe('flush', () => { + test('should skip flush if offline', async () => { + const destination = new Destination(); + destination.config = { + ...useDefaultConfig(), + offline: true, + }; + destination.queue = [ + { + event: { event_type: 'event_type' }, + attempts: 0, + callback: () => undefined, + timeout: 0, + }, + ]; + const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); + await destination.flush(); + expect(send).toHaveBeenCalledTimes(0); + }); + test('should get batch and call send', async () => { const destination = new Destination(); destination.config = { diff --git a/packages/analytics-types/src/client/web-client.ts b/packages/analytics-types/src/client/web-client.ts index c29b9ceb8..f9e34ff33 100644 --- a/packages/analytics-types/src/client/web-client.ts +++ b/packages/analytics-types/src/client/web-client.ts @@ -137,8 +137,6 @@ export interface BrowserClient extends Client { * ``` */ add(plugin: Plugin): AmplitudeReturn; - - setOffline(offline: boolean): void; } export interface ReactNativeClient extends Client { From eeb79e27b8dc5d1e359172a79274a4a6ea756f8d Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 5 Jan 2024 12:03:06 -0800 Subject: [PATCH 05/11] chore: remove log --- packages/analytics-browser/src/plugins/network-checker-plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics-browser/src/plugins/network-checker-plugin.ts b/packages/analytics-browser/src/plugins/network-checker-plugin.ts index efab8b424..a8ec5c1b7 100644 --- a/packages/analytics-browser/src/plugins/network-checker-plugin.ts +++ b/packages/analytics-browser/src/plugins/network-checker-plugin.ts @@ -16,7 +16,6 @@ export class NetworkCheckerPlugin implements BeforePlugin { } async execute(context: Event): Promise { - console.log('NetworkCheckerPlugin execute, offline: ', !navigator.onLine); this.config.offline = !navigator.onLine; return context; } From 8d14bfc77bb7a7c98fd82309b9cd9a4e990f80bd Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 12 Jan 2024 15:59:07 -0800 Subject: [PATCH 06/11] feat: offline mode Move offline logic into a plugin --- .../analytics-browser/src/browser-client.ts | 23 ++----- packages/analytics-browser/src/config.ts | 2 +- .../src/plugins/network-checker-plugin.ts | 22 ------- .../plugins/network-connectivity-checker.ts | 61 +++++++++++++++++++ .../test/browser-client.test.ts | 25 ++++++-- .../network-connectivity-checker.test.ts | 46 ++++++++++++++ packages/analytics-core/src/config.ts | 4 +- packages/analytics-types/src/config/core.ts | 2 +- 8 files changed, 136 insertions(+), 49 deletions(-) delete mode 100644 packages/analytics-browser/src/plugins/network-checker-plugin.ts create mode 100644 packages/analytics-browser/src/plugins/network-connectivity-checker.ts create mode 100644 packages/analytics-browser/test/plugins/network-connectivity-checker.test.ts diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index fec3f8737..895e375fc 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -1,6 +1,5 @@ import { AmplitudeCore, Destination, Identify, returnWrapper, Revenue, UUID } from '@amplitude/analytics-core'; import { - getGlobalScope, getAnalyticsConnector, getAttributionTrackingConfig, getPageViewTrackingConfig, @@ -32,7 +31,7 @@ import { formInteractionTracking } from './plugins/form-interaction-tracking'; import { fileDownloadTracking } from './plugins/file-download-tracking'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_START_EVENT } from './constants'; import { detNotify } from './det-notification'; -import { NetworkCheckerPlugin } from './plugins/network-checker-plugin'; +import { networkConnectivityCheckerPlugin } from './plugins/network-connectivity-checker'; export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -88,7 +87,9 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Step 4: Install plugins // Do not track any events before this - await this.add(new NetworkCheckerPlugin()).promise; + if (this.config.offline !== null) { + await this.add(networkConnectivityCheckerPlugin()).promise; + } await this.add(new Destination()).promise; await this.add(new Context()).promise; await this.add(new IdentityEventSender()).promise; @@ -123,22 +124,6 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { connector.eventBridge.setEventReceiver((event) => { void this.track(event.eventType, event.eventProperties); }); - - // Step 8: Add network listeners for offline mode - const globalScope = getGlobalScope(); - if (globalScope) { - globalScope.addEventListener('online', () => { - this.config.offline = false; - // Flush immediately cause ERR_NETWORK_CHANGED - setTimeout(() => { - this.flush(); - }, this.config.flushIntervalMillis); - }); - - globalScope.addEventListener('offline', () => { - this.config.offline = true; - }); - } } getUserId() { diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 04e05d2f8..a0b473394 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -60,7 +60,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public loggerProvider: ILogger = new Logger(), public logLevel: LogLevel = LogLevel.Warn, public minIdLength?: number, - public offline = false, + public offline: boolean | null = false, optOut = false, public partnerId?: string, public plan?: Plan, diff --git a/packages/analytics-browser/src/plugins/network-checker-plugin.ts b/packages/analytics-browser/src/plugins/network-checker-plugin.ts deleted file mode 100644 index a8ec5c1b7..000000000 --- a/packages/analytics-browser/src/plugins/network-checker-plugin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BeforePlugin, Event } from '@amplitude/analytics-types'; -import { BrowserConfig } from 'src/config'; - -export class NetworkCheckerPlugin implements BeforePlugin { - name = '@amplitude/plugin-network-checker-browser'; - type = 'before' as const; - - // this.config is defined in setup() which will always be called first - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - config: BrowserConfig; - - setup(config: BrowserConfig): Promise { - this.config = config; - return Promise.resolve(undefined); - } - - async execute(context: Event): Promise { - this.config.offline = !navigator.onLine; - return context; - } -} diff --git a/packages/analytics-browser/src/plugins/network-connectivity-checker.ts b/packages/analytics-browser/src/plugins/network-connectivity-checker.ts new file mode 100644 index 000000000..fb02c9045 --- /dev/null +++ b/packages/analytics-browser/src/plugins/network-connectivity-checker.ts @@ -0,0 +1,61 @@ +import { getGlobalScope } from '@amplitude/analytics-client-common'; +import { BeforePlugin, BrowserClient } from '@amplitude/analytics-types'; +import { BrowserConfig } from 'src/config'; + +interface EventListener { + type: 'online' | 'offline'; + handler: () => void; +} + +export const networkConnectivityCheckerPlugin = (): BeforePlugin => { + const name = '@amplitude/plugin-network-checker-browser'; + const type = 'before' as const; + const globalScope = getGlobalScope(); + let eventListeners: EventListener[] = []; + + const addNetworkListener = (type: 'online' | 'offline', handler: () => void) => { + if (globalScope) { + globalScope.addEventListener(type, handler); + eventListeners.push({ + type, + handler, + }); + } + }; + + const removeNetworkListeners = () => { + eventListeners.forEach(({ type, handler }) => { + if (globalScope) { + globalScope.removeEventListener(type, handler); + } + }); + eventListeners = []; + }; + + const setup = async (config: BrowserConfig, amplitude: BrowserClient) => { + config.offline = !navigator.onLine; + + addNetworkListener('online', () => { + config.offline = false; + // Flush immediately will cause ERR_NETWORK_CHANGED + setTimeout(() => { + amplitude.flush(); + }, config.flushIntervalMillis); + }); + + addNetworkListener('offline', () => { + config.offline = true; + }); + }; + + const teardown = async () => { + removeNetworkListeners(); + }; + + return { + name, + type, + setup, + teardown, + }; +}; diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 538c18825..87df8ca44 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -14,6 +14,7 @@ import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; import * as fileDownloadTracking from '../src/plugins/file-download-tracking'; import * as formInteractionTracking from '../src/plugins/form-interaction-tracking'; import * as webAttributionPlugin from '@amplitude/plugin-web-attribution-browser'; +import * as networkConnectivityChecker from '../src/plugins/network-connectivity-checker'; describe('browser-client', () => { let apiKey = ''; @@ -263,10 +264,29 @@ describe('browser-client', () => { expect(webAttributionPluginPlugin).toHaveBeenCalledTimes(1); }); + test('should add network connectivity checker plugin by default', async () => { + const networkConnectivityCheckerPlugin = jest.spyOn( + networkConnectivityChecker, + 'networkConnectivityCheckerPlugin', + ); + await client.init(apiKey, userId).promise; + expect(networkConnectivityCheckerPlugin).toHaveBeenCalledTimes(1); + }); + + test('should not add network connectivity checker plugin if offline is null', async () => { + const networkConnectivityCheckerPlugin = jest.spyOn( + networkConnectivityChecker, + 'networkConnectivityCheckerPlugin', + ); + await client.init(apiKey, userId, { + offline: null, + }).promise; + expect(networkConnectivityCheckerPlugin).toHaveBeenCalledTimes(0); + }); + test('should listen for network change to online', async () => { jest.useFakeTimers(); const addEventListenerMock = jest.spyOn(window, 'addEventListener'); - // const setTimeoutSpy = jest.spyOn(window, 'setTimeout'); const flush = jest.spyOn(client, 'flush').mockReturnValue({ promise: Promise.resolve() }); await client.init(apiKey, { @@ -276,15 +296,12 @@ describe('browser-client', () => { expect(addEventListenerMock).toHaveBeenCalledWith('online', expect.any(Function)); expect(client.config.offline).toBe(false); - // expect(setTimeoutSpy).toHaveBeenCalledTimes(1); - // expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), client.config.flushIntervalMillis); jest.advanceTimersByTime(client.config.flushIntervalMillis); expect(flush).toHaveBeenCalledTimes(1); jest.useRealTimers(); addEventListenerMock.mockRestore(); - // setTimeoutSpy.mockRestore(); flush.mockRestore(); }); diff --git a/packages/analytics-browser/test/plugins/network-connectivity-checker.test.ts b/packages/analytics-browser/test/plugins/network-connectivity-checker.test.ts new file mode 100644 index 000000000..ff0387675 --- /dev/null +++ b/packages/analytics-browser/test/plugins/network-connectivity-checker.test.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/unbound-method */ + +import { createAmplitudeMock, createConfigurationMock } from '../helpers/mock'; +import { networkConnectivityCheckerPlugin } from '../../src/plugins/network-connectivity-checker'; + +describe('networkConnectivityCheckerPlugin', () => { + const amplitude = createAmplitudeMock(); + const config = createConfigurationMock(); + + test('should set up correctly when online', async () => { + const plugin = networkConnectivityCheckerPlugin(); + jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(true); + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + + await plugin.setup?.(config, amplitude); + + expect(config.offline).toEqual(false); + expect(addEventListenerSpy).toHaveBeenCalledWith('online', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith('offline', expect.any(Function)); + addEventListenerSpy.mockRestore(); + }); + + test('should set up correctly when offline', async () => { + const plugin = networkConnectivityCheckerPlugin(); + jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(false); + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + + await plugin.setup?.(config, amplitude); + + expect(config.offline).toEqual(true); + expect(addEventListenerSpy).toHaveBeenCalledWith('online', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith('offline', expect.any(Function)); + addEventListenerSpy.mockRestore(); + }); + + test('should teardown plugin', async () => { + const plugin = networkConnectivityCheckerPlugin(); + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + await plugin.setup?.(createConfigurationMock(), amplitude); + await plugin.teardown?.(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('online', expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith('offline', expect.any(Function)); + }); +}); diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index 2c586c78a..12cca3363 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -42,7 +42,7 @@ export class Config implements IConfig { loggerProvider: ILogger; logLevel: LogLevel; minIdLength?: number; - offline: boolean; + offline?: boolean | null; plan?: Plan; ingestionMetadata?: IngestionMetadata; serverUrl: string | undefined; @@ -71,7 +71,7 @@ export class Config implements IConfig { this.minIdLength = options.minIdLength; this.plan = options.plan; this.ingestionMetadata = options.ingestionMetadata; - this.offline = options.offline ?? defaultConfig.offline; + this.offline = options.offline !== undefined ? options.offline : defaultConfig.offline; this.optOut = options.optOut ?? defaultConfig.optOut; this.serverUrl = options.serverUrl; this.serverZone = options.serverZone || defaultConfig.serverZone; diff --git a/packages/analytics-types/src/config/core.ts b/packages/analytics-types/src/config/core.ts index 5fe79ea4f..72648d607 100644 --- a/packages/analytics-types/src/config/core.ts +++ b/packages/analytics-types/src/config/core.ts @@ -15,7 +15,7 @@ export interface Config { logLevel: LogLevel; loggerProvider: Logger; minIdLength?: number; - offline: boolean; + offline?: boolean | null; optOut: boolean; plan?: Plan; ingestionMetadata?: IngestionMetadata; From b96e45f01a84bb06df90382d0748c562ab5bf0b3 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Mon, 22 Jan 2024 14:32:49 -0800 Subject: [PATCH 07/11] feat: offline mode Co-authored-by: Justin Fiedler --- packages/analytics-core/test/plugins/destination.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 8f3a43e91..44faab600 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -144,7 +144,7 @@ describe('destination', () => { expect(flush).toHaveBeenCalledTimes(2); }); - test('should not schedule a flush', async () => { + test('should not schedule a flush if offline', async () => { const destination = new Destination(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (destination as any).scheduled = null; From 3da26db54eb14d428bc7d04d2c9f3b9cb9612362 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 23 Jan 2024 14:53:01 -0800 Subject: [PATCH 08/11] feat: offline mode export OfflineDisabled --- packages/analytics-browser/src/browser-client.ts | 3 ++- packages/analytics-browser/test/browser-client.test.ts | 6 +++--- packages/analytics-core/src/config.ts | 3 ++- packages/analytics-types/src/config/core.ts | 3 ++- packages/analytics-types/src/index.ts | 1 + packages/analytics-types/src/offline.ts | 1 + 6 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 packages/analytics-types/src/offline.ts diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 895e375fc..b79735313 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -21,6 +21,7 @@ import { Identify as IIdentify, Revenue as IRevenue, TransportType, + OfflineDisabled, } from '@amplitude/analytics-types'; import { convertProxyObjectToRealObject, isInstanceProxy } from './utils/snippet-helper'; import { Context } from './plugins/context'; @@ -87,7 +88,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Step 4: Install plugins // Do not track any events before this - if (this.config.offline !== null) { + if (this.config.offline !== OfflineDisabled) { await this.add(networkConnectivityCheckerPlugin()).promise; } await this.add(new Destination()).promise; diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 87df8ca44..04ac004c1 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -2,7 +2,7 @@ import { AmplitudeBrowser } from '../src/browser-client'; import * as core from '@amplitude/analytics-core'; import * as Config from '../src/config'; import * as CookieMigration from '../src/cookie-migration'; -import { UserSession } from '@amplitude/analytics-types'; +import { OfflineDisabled, UserSession } from '@amplitude/analytics-types'; import { CookieStorage, FetchTransport, @@ -273,13 +273,13 @@ describe('browser-client', () => { expect(networkConnectivityCheckerPlugin).toHaveBeenCalledTimes(1); }); - test('should not add network connectivity checker plugin if offline is null', async () => { + test('should not add network connectivity checker plugin if offline is disabled', async () => { const networkConnectivityCheckerPlugin = jest.spyOn( networkConnectivityChecker, 'networkConnectivityCheckerPlugin', ); await client.init(apiKey, userId, { - offline: null, + offline: OfflineDisabled, }).promise; expect(networkConnectivityCheckerPlugin).toHaveBeenCalledTimes(0); }); diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index 12cca3363..09b44fd23 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -9,6 +9,7 @@ import { IngestionMetadata, Options, ServerZoneType, + OfflineDisabled, } from '@amplitude/analytics-types'; import { AMPLITUDE_SERVER_URL, @@ -42,7 +43,7 @@ export class Config implements IConfig { loggerProvider: ILogger; logLevel: LogLevel; minIdLength?: number; - offline?: boolean | null; + offline?: boolean | typeof OfflineDisabled; plan?: Plan; ingestionMetadata?: IngestionMetadata; serverUrl: string | undefined; diff --git a/packages/analytics-types/src/config/core.ts b/packages/analytics-types/src/config/core.ts index 72648d607..aeeaf7530 100644 --- a/packages/analytics-types/src/config/core.ts +++ b/packages/analytics-types/src/config/core.ts @@ -5,6 +5,7 @@ import { ServerZoneType } from '../server-zone'; import { Storage } from '../storage'; import { Transport } from '../transport'; import { Logger, LogLevel } from '../logger'; +import { OfflineDisabled } from '../offline'; export interface Config { apiKey: string; @@ -15,7 +16,7 @@ export interface Config { logLevel: LogLevel; loggerProvider: Logger; minIdLength?: number; - offline?: boolean | null; + offline?: boolean | typeof OfflineDisabled; optOut: boolean; plan?: Plan; ingestionMetadata?: IngestionMetadata; diff --git a/packages/analytics-types/src/index.ts b/packages/analytics-types/src/index.ts index 8d5364b7e..5d51c8f65 100644 --- a/packages/analytics-types/src/index.ts +++ b/packages/analytics-types/src/index.ts @@ -59,3 +59,4 @@ export { Transport, TransportType } from './transport'; export { UserSession } from './user-session'; export { UTMData } from './utm'; export { PageTrackingOptions, PageTrackingTrackOn, PageTrackingHistoryChanges } from './page-view-tracking'; +export { OfflineDisabled } from './offline'; diff --git a/packages/analytics-types/src/offline.ts b/packages/analytics-types/src/offline.ts new file mode 100644 index 000000000..2d1a6881b --- /dev/null +++ b/packages/analytics-types/src/offline.ts @@ -0,0 +1 @@ +export const OfflineDisabled = null; From a2505ec7adfa5d360ef121570854fe89b3100bd2 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 23 Jan 2024 15:06:55 -0800 Subject: [PATCH 09/11] feat: offline mode --- packages/analytics-browser/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index a0b473394..37572515d 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -60,7 +60,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public loggerProvider: ILogger = new Logger(), public logLevel: LogLevel = LogLevel.Warn, public minIdLength?: number, - public offline: boolean | null = false, + public offline: boolean | Offline.Disabled = false, optOut = false, public partnerId?: string, public plan?: Plan, From 14e8e4cc7772907fe547feaade9a0a2c4314b78d Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 23 Jan 2024 15:08:38 -0800 Subject: [PATCH 10/11] feat: offline mode --- packages/analytics-browser/src/config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 37572515d..9ffb0906f 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -13,6 +13,7 @@ import { IngestionMetadata, IdentityStorageType, ServerZoneType, + OfflineDisabled, } from '@amplitude/analytics-types'; import { Config, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { CookieStorage, getCookieName, FetchTransport, getQueryParams } from '@amplitude/analytics-client-common'; @@ -60,7 +61,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public loggerProvider: ILogger = new Logger(), public logLevel: LogLevel = LogLevel.Warn, public minIdLength?: number, - public offline: boolean | Offline.Disabled = false, + public offline: boolean | typeof OfflineDisabled = false, optOut = false, public partnerId?: string, public plan?: Plan, From c2c585304a4e6381c648fe471e9603cc5accd735 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 24 Jan 2024 11:58:16 -0800 Subject: [PATCH 11/11] feat: offline mode add debug messages --- .../plugins/network-connectivity-checker.ts | 2 ++ .../test/browser-client.test.ts | 22 +++++++++++++++++++ .../analytics-core/src/plugins/destination.ts | 1 + .../test/plugins/destination.test.ts | 12 ++++++++++ 4 files changed, 37 insertions(+) diff --git a/packages/analytics-browser/src/plugins/network-connectivity-checker.ts b/packages/analytics-browser/src/plugins/network-connectivity-checker.ts index fb02c9045..4581d89c1 100644 --- a/packages/analytics-browser/src/plugins/network-connectivity-checker.ts +++ b/packages/analytics-browser/src/plugins/network-connectivity-checker.ts @@ -36,6 +36,7 @@ export const networkConnectivityCheckerPlugin = (): BeforePlugin => { config.offline = !navigator.onLine; addNetworkListener('online', () => { + config.loggerProvider.debug('Network connectivity changed to online.'); config.offline = false; // Flush immediately will cause ERR_NETWORK_CHANGED setTimeout(() => { @@ -44,6 +45,7 @@ export const networkConnectivityCheckerPlugin = (): BeforePlugin => { }); addNetworkListener('offline', () => { + config.loggerProvider.debug('Network connectivity changed to offline.'); config.offline = true; }); }; diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 04ac004c1..bcf68779a 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -288,14 +288,25 @@ describe('browser-client', () => { jest.useFakeTimers(); const addEventListenerMock = jest.spyOn(window, 'addEventListener'); const flush = jest.spyOn(client, 'flush').mockReturnValue({ promise: Promise.resolve() }); + const loggerProvider = { + log: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), + }; await client.init(apiKey, { defaultTracking: false, + loggerProvider: loggerProvider, }).promise; window.dispatchEvent(new Event('online')); expect(addEventListenerMock).toHaveBeenCalledWith('online', expect.any(Function)); expect(client.config.offline).toBe(false); + expect(loggerProvider.debug).toHaveBeenCalledTimes(1); + expect(loggerProvider.debug).toHaveBeenCalledWith('Network connectivity changed to online.'); jest.advanceTimersByTime(client.config.flushIntervalMillis); expect(flush).toHaveBeenCalledTimes(1); @@ -308,15 +319,26 @@ describe('browser-client', () => { test('should listen for network change to offline', async () => { jest.useFakeTimers(); const addEventListenerMock = jest.spyOn(window, 'addEventListener'); + const loggerProvider = { + log: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), + }; await client.init(apiKey, { defaultTracking: false, + loggerProvider: loggerProvider, }).promise; expect(client.config.offline).toBe(false); window.dispatchEvent(new Event('offline')); expect(addEventListenerMock).toHaveBeenCalledWith('offline', expect.any(Function)); expect(client.config.offline).toBe(true); + expect(loggerProvider.debug).toHaveBeenCalledTimes(1); + expect(loggerProvider.debug).toHaveBeenCalledWith('Network connectivity changed to offline.'); jest.useRealTimers(); addEventListenerMock.mockRestore(); diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index d5d0a5df6..a67c27bce 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -122,6 +122,7 @@ export class Destination implements DestinationPlugin { async flush(useRetry = false) { // Skip flush if offline if (this.config.offline) { + this.config.loggerProvider.debug('Skipping flush while offline.'); return; } diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 44faab600..e4ed63e99 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -190,10 +190,20 @@ describe('destination', () => { describe('flush', () => { test('should skip flush if offline', async () => { + const loggerProvider = { + log: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), + }; + const destination = new Destination(); destination.config = { ...useDefaultConfig(), offline: true, + loggerProvider: loggerProvider, }; destination.queue = [ { @@ -206,6 +216,8 @@ describe('destination', () => { const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); await destination.flush(); expect(send).toHaveBeenCalledTimes(0); + expect(loggerProvider.debug).toHaveBeenCalledTimes(1); + expect(loggerProvider.debug).toHaveBeenCalledWith('Skipping flush while offline.'); }); test('should get batch and call send', async () => {