Skip to content

Commit

Permalink
feat: offline mode
Browse files Browse the repository at this point in the history
Move offline logic into a plugin
  • Loading branch information
Mercy811 committed Jan 13, 2024
1 parent eeb79e2 commit 8d14bfc
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 49 deletions.
23 changes: 4 additions & 19 deletions packages/analytics-browser/src/browser-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AmplitudeCore, Destination, Identify, returnWrapper, Revenue, UUID } from '@amplitude/analytics-core';
import {
getGlobalScope,
getAnalyticsConnector,
getAttributionTrackingConfig,
getPageViewTrackingConfig,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion packages/analytics-browser/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 0 additions & 22 deletions packages/analytics-browser/src/plugins/network-checker-plugin.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,
};
};
25 changes: 21 additions & 4 deletions packages/analytics-browser/test/browser-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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, {
Expand All @@ -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();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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));
});
});
4 changes: 2 additions & 2 deletions packages/analytics-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/analytics-types/src/config/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface Config {
logLevel: LogLevel;
loggerProvider: Logger;
minIdLength?: number;
offline: boolean;
offline?: boolean | null;
optOut: boolean;
plan?: Plan;
ingestionMetadata?: IngestionMetadata;
Expand Down

0 comments on commit 8d14bfc

Please sign in to comment.