diff --git a/README.md b/README.md index f5044b6e..90de5097 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,12 @@ The hassle-free way to add Segment analytics to your React-Native app. ## Installation -Install `@segment/analytics-react-native`, [`@segment/sovran-react-native`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran) and [`react-native-get-random-values`](https://github.com/LinusU/react-native-get-random-values): +Install `@segment/analytics-react-native`, [`@segment/sovran-react-native`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran), [`react-native-get-random-values`](https://github.com/LinusU/react-native-get-random-values) and [`@react-native-community/netinfo`](https://github.com/react-native-netinfo/react-native-netinfo): ```sh -yarn add @segment/analytics-react-native @segment/sovran-react-native react-native-get-random-values +yarn add @segment/analytics-react-native @segment/sovran-react-native react-native-get-random-values @react-native-community/netinfo # or -npm install --save @segment/analytics-react-native @segment/sovran-react-native react-native-get-random-values +npm install --save @segment/analytics-react-native @segment/sovran-react-native react-native-get-random-values @react-native-community/netinfo ``` If you want to use the default persistor for the Segment Analytics client, you also have to install [`react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage). diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index bd0d7bf5..5536a705 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -222,6 +222,8 @@ PODS: - glog - react-native-get-random-values (1.8.0): - React-Core + - react-native-netinfo (9.4.1): + - React-Core - react-native-safe-area-context (3.4.1): - React-Core - react-native-tracking-transparency (0.1.1): @@ -331,6 +333,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-tracking-transparency (from `../node_modules/react-native-tracking-transparency`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -399,6 +402,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-tracking-transparency: @@ -466,6 +471,7 @@ SPEC CHECKSUMS: React-jsinspector: 8134ee22182b8dd98dc0973db6266c398103ce6c React-logger: 1e7ac909607ee65fd5c4d8bea8c6e644f66b8843 react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a + react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5 react-native-safe-area-context: 9e40fb181dac02619414ba1294d6c2a807056ab9 react-native-tracking-transparency: b2029ff756f1128b1f2c7c7c7f3003bc3c950f9f React-perflogger: 8e832d4e21fdfa613033c76d58d7e617341e804b diff --git a/example/package.json b/example/package.json index 41b8b418..6ad29712 100644 --- a/example/package.json +++ b/example/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "^1.15.7", + "@react-native-community/netinfo": "^9.4.1", "@react-native-community/masked-view": "^0.1.11", "@react-navigation/native": "^6.0.2", "@react-navigation/stack": "^6.0.7", diff --git a/example/yarn.lock b/example/yarn.lock index 4aaa0c55..f4c07f83 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1738,6 +1738,11 @@ resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.11.tgz#2f4c6e10bee0786abff4604e39a37ded6f3980ce" integrity sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw== +"@react-native-community/netinfo@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-9.4.1.tgz#7b880758adca65fe47ee866cf7b00416b9dcc192" + integrity sha512-dAbY5mfw+6Kas/GJ6QX9AZyY+K+eq9ad4Su6utoph/nxyH3whp5cMSgRNgE2VhGQVRZ/OG0qq3IaD3+wzoqJXw== + "@react-native/assets@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e" diff --git a/packages/core/package.json b/packages/core/package.json index 5eab343c..bbda842c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,6 +56,7 @@ "peerDependencies": { "react-native-get-random-values": "1.x", "@react-native-async-storage/async-storage": "1.x", + "@react-native-community/netinfo": "9.x", "react": "*", "react-native": "*" }, @@ -68,6 +69,7 @@ "@babel/core": "^7.12.9", "@babel/runtime": "^7.12.5", "@react-native-community/eslint-config": "^2.0.0", + "@react-native-community/netinfo": "^9.4.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/commit-analyzer": "^9.0.2", "@semantic-release/git": "^10.0.1", diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 79cc6090..9b919a99 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -23,6 +23,7 @@ import { CountFlushPolicy, Observable, TimerFlushPolicy, + OnlineFlushPolicy, } from './flushPolicies'; import { FlushPolicyExecuter } from './flushPolicies/flush-policy-executer'; import type { DestinationPlugin, PlatformPlugin, Plugin } from './plugin'; @@ -743,6 +744,8 @@ export class SegmentClient { } } + flushPolicies.push(new OnlineFlushPolicy()); + this.flushPolicyExecuter = new FlushPolicyExecuter(flushPolicies, () => { void this.flush(); }); diff --git a/packages/core/src/flushPolicies/__tests__/online-flush-policy.test.ts b/packages/core/src/flushPolicies/__tests__/online-flush-policy.test.ts new file mode 100644 index 00000000..e8375fca --- /dev/null +++ b/packages/core/src/flushPolicies/__tests__/online-flush-policy.test.ts @@ -0,0 +1,35 @@ +import type { + NetInfoChangeHandler, + NetInfoState, +} from '@react-native-community/netinfo'; +import { OnlineFlushPolicy } from '../online-flush-policy'; + +let netInfoListener: NetInfoChangeHandler; +jest.mock('@react-native-community/netinfo', () => ({ + addEventListener: (cb: NetInfoChangeHandler) => { + netInfoListener = cb; + }, +})); + +describe('OnlineFlushPolicy', () => { + it('triggers a flush when device (re-)connects to network', () => { + const policy = new OnlineFlushPolicy(); + + policy.start(); + + const observer = jest.fn(); + + policy.shouldFlush.onChange(observer); + + policy.onEvent(); + policy.onEvent(); + policy.onEvent(); + + expect(observer).not.toHaveBeenCalled(); + + // lets signal that the device is now connected + netInfoListener({ isConnected: true } as NetInfoState); + + expect(observer).toHaveBeenCalledWith(true); + }); +}); diff --git a/packages/core/src/flushPolicies/index.ts b/packages/core/src/flushPolicies/index.ts index e340aaf0..f36360b6 100644 --- a/packages/core/src/flushPolicies/index.ts +++ b/packages/core/src/flushPolicies/index.ts @@ -3,3 +3,4 @@ export * from './count-flush-policy'; export * from './timer-flush-policy'; export * from './startup-flush-policy'; export * from './background-flush-policy'; +export * from './online-flush-policy'; diff --git a/packages/core/src/flushPolicies/online-flush-policy.ts b/packages/core/src/flushPolicies/online-flush-policy.ts new file mode 100644 index 00000000..273970bd --- /dev/null +++ b/packages/core/src/flushPolicies/online-flush-policy.ts @@ -0,0 +1,25 @@ +import NetInfo from '@react-native-community/netinfo'; +import { FlushPolicyBase } from './types'; + +/** + * OnlineFlushPolicy uploads events when the device (re-)connects to network + */ +export class OnlineFlushPolicy extends FlushPolicyBase { + private unsubscribe: (() => void) | undefined; + + start(): void { + this.unsubscribe = NetInfo.addEventListener((state) => { + if (state.isConnected === true) { + this.shouldFlush.value = true; + } + }); + } + + end(): void { + this.unsubscribe?.(); + } + + onEvent(): void { + // not applicable + } +} diff --git a/packages/core/src/plugins/SegmentDestination.ts b/packages/core/src/plugins/SegmentDestination.ts index 93f0cbcb..7062bef9 100644 --- a/packages/core/src/plugins/SegmentDestination.ts +++ b/packages/core/src/plugins/SegmentDestination.ts @@ -1,3 +1,4 @@ +import NetInfo from '@react-native-community/netinfo'; import { DestinationPlugin } from '../plugin'; import { PluginType, @@ -26,7 +27,7 @@ export class SegmentDestination extends DestinationPlugin { private isReady = false; private sendEvents = async (events: SegmentEvent[]): Promise => { - if (!this.isReady) { + if (!this.isReady || (await NetInfo.fetch()).isConnected === false) { // We're not sending events until Segment has loaded all settings return Promise.resolve(); } diff --git a/yarn.lock b/yarn.lock index 3cfa7934..60801123 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2403,6 +2403,11 @@ resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.2.0.tgz#7d6d789ae8edf73dc9bed1246cd48277edea8066" integrity sha512-o6aam+0Ug1xGK3ABYmBm0B1YuEKfM/5kaoZO0eHbZwSpw9UzDX4G5y4Nx/K20FHqUmJHkZmLvOUFYwN4N+HqKA== +"@react-native-community/netinfo@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-9.4.1.tgz#7b880758adca65fe47ee866cf7b00416b9dcc192" + integrity sha512-dAbY5mfw+6Kas/GJ6QX9AZyY+K+eq9ad4Su6utoph/nxyH3whp5cMSgRNgE2VhGQVRZ/OG0qq3IaD3+wzoqJXw== + "@react-native-firebase/analytics@^17.3.2": version "17.3.2" resolved "https://registry.yarnpkg.com/@react-native-firebase/analytics/-/analytics-17.3.2.tgz#31ddb8b349f073b540d6e15ac2c4216439b39390"