diff --git a/packages/core/android/src/main/java/com/segment/analytics/reactnative/core/RNAnalyticsModule.kt b/packages/core/android/src/main/java/com/segment/analytics/reactnative/core/RNAnalyticsModule.kt index 8e130413e..db4b17a69 100644 --- a/packages/core/android/src/main/java/com/segment/analytics/reactnative/core/RNAnalyticsModule.kt +++ b/packages/core/android/src/main/java/com/segment/analytics/reactnative/core/RNAnalyticsModule.kt @@ -24,10 +24,7 @@ package com.segment.analytics.reactnative.core -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.* import com.segment.analytics.Analytics import com.segment.analytics.Properties import com.segment.analytics.Traits @@ -35,13 +32,28 @@ import com.segment.analytics.ValueMap import java.util.concurrent.TimeUnit class RNAnalyticsModule(context: ReactApplicationContext): ReactContextBaseJavaModule(context) { + override fun getName() = "RNAnalytics" + private val analytics get() = Analytics.with(reactApplicationContext) - override fun getName() = "RNAnalytics" + companion object { + private var singletonJsonConfig: String? = null + } @ReactMethod - fun setup(options: ReadableMap) { + fun setup(options: ReadableMap, promise: Promise) { + val json = options.getString("json") + + if(singletonJsonConfig != null) { + if(json == singletonJsonConfig) { + return promise.resolve(null) + } + else { + return promise.reject("E_SEGMENT_RECONFIGURED", "Duplicate Analytics client") + } + } + val builder = Analytics .Builder(reactApplicationContext, options.getString("writeKey")) .flushQueueSize(options.getInt("flushAt")) @@ -69,9 +81,16 @@ class RNAnalyticsModule(context: ReactApplicationContext): ReactContextBaseJavaM builder.logLevel(Analytics.LogLevel.VERBOSE) } - Analytics.setSingletonInstance( - RNAnalytics.buildWithIntegrations(builder) - ) + try { + Analytics.setSingletonInstance( + RNAnalytics.buildWithIntegrations(builder) + ) + } catch(e: Exception) { + return promise.reject("E_SEGMENT_ERROR", e) + } + + singletonJsonConfig = json + promise.resolve(null) } @ReactMethod diff --git a/packages/core/docs/classes/analytics.client.md b/packages/core/docs/classes/analytics.client.md index dce5aa518..1962cbfa8 100644 --- a/packages/core/docs/classes/analytics.client.md +++ b/packages/core/docs/classes/analytics.client.md @@ -54,7 +54,7 @@ ___ ▸ **alias**(newId: *`string`*): `Promise`<`void`> -*Defined in [analytics.ts:260](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L260)* +*Defined in [analytics.ts:261](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L261)* Merge two user identities, effectively connecting two sets of user data as one. This may not be supported by all integrations. @@ -96,7 +96,7 @@ ___ ▸ **disable**(): `Promise`<`void`> -*Defined in [analytics.ts:299](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L299)* +*Defined in [analytics.ts:300](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L300)* Completely disable the sending of any analytics data. @@ -111,7 +111,7 @@ ___ ▸ **enable**(): `Promise`<`void`> -*Defined in [analytics.ts:289](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L289)* +*Defined in [analytics.ts:290](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L290)* Enable the sending of analytics data. Enabled by default. @@ -126,7 +126,7 @@ ___ ▸ **flush**(): `Promise`<`void`> -*Defined in [analytics.ts:280](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L280)* +*Defined in [analytics.ts:281](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L281)* Trigger an upload of all queued events. @@ -141,7 +141,7 @@ ___ ▸ **group**(groupId: *`string`*, traits?: *[JsonMap]()*): `Promise`<`void`> -*Defined in [analytics.ts:247](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L247)* +*Defined in [analytics.ts:248](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L248)* Associate a user with a group, organization, company, project, or w/e _you_ call them. @@ -163,7 +163,7 @@ ___ ▸ **identify**(user: *`string`*, traits?: *[JsonMap]()*): `Promise`<`void`> -*Defined in [analytics.ts:235](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L235)* +*Defined in [analytics.ts:236](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L236)* Associate a user with their unique ID and record traits about them. @@ -223,7 +223,7 @@ ___ ▸ **reset**(): `Promise`<`void`> -*Defined in [analytics.ts:270](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L270)* +*Defined in [analytics.ts:271](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L271)* Reset any user state that is cached on the device. @@ -238,7 +238,7 @@ ___ ▸ **screen**(name: *`string`*, properties?: *[JsonMap]()*): `Promise`<`void`> -*Defined in [analytics.ts:221](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L221)* +*Defined in [analytics.ts:222](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L222)* Record the screens or views your users see. @@ -290,7 +290,7 @@ ___ ▸ **track**(event: *`string`*, properties?: *[JsonMap]()*): `Promise`<`void`> -*Defined in [analytics.ts:203](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L203)* +*Defined in [analytics.ts:204](https://github.com/segmentio/analytics-react-native/blob/master/packages/core/src/analytics.ts#L204)* Record the actions your users perform. diff --git a/packages/core/ios/RNAnalytics/RNAnalytics.m b/packages/core/ios/RNAnalytics/RNAnalytics.m index b006d7729..3fa2cacd4 100644 --- a/packages/core/ios/RNAnalytics/RNAnalytics.m +++ b/packages/core/ios/RNAnalytics/RNAnalytics.m @@ -32,7 +32,24 @@ +(void)initialize { @synthesize bridge = _bridge; -RCT_EXPORT_METHOD(setup:(NSDictionary*)options) { +static NSString* singletonJsonConfig = nil; + +RCT_EXPORT_METHOD( + setup:(NSDictionary*)options + :(RCTPromiseResolveBlock)resolver + :(RCTPromiseRejectBlock)rejecter +) { + NSString* json = options[@"json"]; + + if(singletonJsonConfig != nil) { + if([json isEqualToString:singletonJsonConfig]) { + return resolver(nil); + } + else { + return rejecter(@"E_SEGMENT_RECONFIGURED", @"Duplicate Analytics client", nil); + } + } + SEGAnalyticsConfiguration* config = [SEGAnalyticsConfiguration configurationWithWriteKey:options[@"writeKey"]]; config.recordScreenViews = [options[@"recordScreenViews"] boolValue]; @@ -46,7 +63,13 @@ +(void)initialize { } [SEGAnalytics debug:[options[@"debug"] boolValue]]; - [SEGAnalytics setupWithConfiguration:config]; + + @try { + [SEGAnalytics setupWithConfiguration:config]; + } + @catch(NSException* error) { + return rejecter(@"E_SEGMENT_ERROR", @"Unexpected native Analtyics error", error); + } // On iOS we use method swizzling to intercept lifecycle events // However, React-Native calls our library after applicationDidFinishLaunchingWithOptions: is called @@ -60,6 +83,9 @@ +(void)initialize { withObject:_bridge.launchOptions]; } } + + singletonJsonConfig = json; + resolver(nil); } #define withContext(context) @{@"context": context} diff --git a/packages/core/src/__tests__/configuration.spec.ts b/packages/core/src/__tests__/configuration.spec.ts index 08d1a444a..0b8689581 100644 --- a/packages/core/src/__tests__/configuration.spec.ts +++ b/packages/core/src/__tests__/configuration.spec.ts @@ -2,24 +2,35 @@ import { configure } from '../configuration' const writeKey = 'test-write-key' +function withIntegrity(config: T): T & { json: string } { + const json = JSON.stringify(config) + + return { + ...(config as any), + json + } +} + it('uses the default configuration', async () => { - expect(await configure(writeKey, {})).toEqual({ - debug: false, - flushAt: 20, - recordScreenViews: false, - trackAppLifecycleEvents: false, - trackAttributionData: false, - writeKey, + expect(await configure(writeKey, {})).toEqual( + withIntegrity({ + debug: false, + flushAt: 20, + recordScreenViews: false, + trackAppLifecycleEvents: false, + trackAttributionData: false, + writeKey, - android: { - collectDeviceId: true, - flushInterval: undefined - }, - ios: { - trackAdvertising: false, - trackDeepLinks: false - } - }) + android: { + collectDeviceId: true, + flushInterval: undefined + }, + ios: { + trackAdvertising: false, + trackDeepLinks: false + } + }) + ) }) it('produces a valid configuration', async () => { @@ -40,23 +51,25 @@ it('produces a valid configuration', async () => { } }) - expect(config).toEqual({ - debug: true, - flushAt: 42, - recordScreenViews: true, - trackAppLifecycleEvents: true, - trackAttributionData: true, - writeKey, + expect(config).toEqual( + withIntegrity({ + debug: true, + flushAt: 42, + recordScreenViews: true, + trackAppLifecycleEvents: true, + trackAttributionData: true, + writeKey, - android: { - collectDeviceId: false, - flushInterval: 72 - }, - ios: { - trackAdvertising: true, - trackDeepLinks: true - } - }) + android: { + collectDeviceId: false, + flushInterval: 72 + }, + ios: { + trackAdvertising: true, + trackDeepLinks: true + } + }) + ) }) it('waits for integrations to register', async () => { diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 44ba64b13..3084be91f 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -184,8 +184,9 @@ export module Analytics { * @param configuration An optional {@link Configuration} object. */ public async setup(writeKey: string, configuration: Configuration = {}) { - await Bridge.setup(await configure(writeKey, configuration)) - + await Bridge.setup( + await configure(writeKey, configuration) + ) this.wrapper.ready() } diff --git a/packages/core/src/bridge.ts b/packages/core/src/bridge.ts index 13f007fd3..0583cc586 100644 --- a/packages/core/src/bridge.ts +++ b/packages/core/src/bridge.ts @@ -7,6 +7,7 @@ export interface Configuration { trackAttributionData: boolean debug: boolean flushAt: number + json: string android: { flushInterval?: number diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index d76128f9c..d3d208f67 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -38,7 +38,7 @@ export const configure = async ( ) ) - return { + const config = { debug, flushAt, recordScreenViews, @@ -49,4 +49,10 @@ export const configure = async ( android: defaults.android(android), ios: defaults.ios(ios) } + const json = JSON.stringify(config) + + return { + ...config, + json + } }