diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0e932b55..13b6ed46 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -286,11 +286,11 @@ PODS: - React - RNGestureHandler (2.2.0): - React-Core - - segment-analytics-react-native (2.1.2-beta): + - segment-analytics-react-native (2.1.4-beta): - React-Core - segment-analytics-react-native-plugin-idfa (0.2.0-beta): - React-Core - - sovran-react-native (0.2.3): + - sovran-react-native (0.2.4): - React-Core - Yoga (1.14.0) @@ -458,9 +458,9 @@ SPEC CHECKSUMS: RNCAsyncStorage: b49b4e38a1548d03b74b30e558a1d18465b94be7 RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNGestureHandler: bf572f552ea324acd5b5464b8d30755b2d8c1de6 - segment-analytics-react-native: 76fa77e887ea38d063e01ec8877eaaf39745790d + segment-analytics-react-native: cafec7a2e5f20b4fb6e872f8055574e982c7bb0f segment-analytics-react-native-plugin-idfa: 2dc6e38506a5b034db4a4cf16db48643b2f356a2 - sovran-react-native: 814ebda5c04a60a4f9eea1b203b95f2f64bca291 + sovran-react-native: 1b68d70aaa2d96489e0338eaf3a4cbf92688c793 Yoga: 3f5bfc54ce164fcd5b5d7f9f4232182d6298dd56 PODFILE CHECKSUM: 0c7eb82d495ca56953c50916b7b49e7512632eb6 diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 9545c0e0..0c630fde 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -54,9 +54,6 @@ export class SegmentClient { // internal time to know when to flush, ticks every second private flushInterval: ReturnType | null = null; - // Watcher for isReady updates to the storage - private readinessWatcher?: Unsubscribe = undefined; - // unsubscribe watchers for the store private watchers: Unsubscribe[] = []; @@ -70,8 +67,8 @@ export class SegmentClient { private timeline: Timeline; - // mechanism to prevent adding plugins before we are fully initalised - private isStorageReady = false; + private pendingEvents: SegmentEvent[] = []; + private pluginsToAdd: Plugin[] = []; private isInitialized = false; @@ -167,9 +164,6 @@ export class SegmentClient { this.add({ plugin: segmentDestination }); } - // Setup platform specific plugins - this.platformPlugins.forEach((plugin) => this.add({ plugin: plugin })); - // Initialize the watchables this.context = { get: this.store.context.get, @@ -199,6 +193,13 @@ export class SegmentClient { get: this.store.events.get, onChange: this.store.events.onChange, }; + + // Watch for isReady so that we can handle any pending events + // Delays events processing in the timeline until the store is ready to prevent missing data injected from the plugins + this.store.isReady.onChange((value) => this.onStorageReady(value)); + + // Setup platform specific plugins + this.platformPlugins.forEach((plugin) => this.add({ plugin: plugin })); } /** @@ -211,12 +212,6 @@ export class SegmentClient { return; } - // Plugin interval check - if (this.store.isReady.get()) { - this.onStorageReady(true); - } else { - this.store.isReady.onChange((value) => this.onStorageReady(value)); - } await this.fetchSettings(); // flush any stored events @@ -290,7 +285,6 @@ export class SegmentClient { clearInterval(this.flushInterval); } - this.unsubscribeReadinessWatcher(); this.unsubscribeStorageWatchers(); this.appStateSubscription?.remove(); @@ -358,7 +352,7 @@ export class SegmentClient { this.store.settings.add((plugin as DestinationPlugin).key, settings); } - if (!this.isStorageReady) { + if (!this.store.isReady.get()) { this.pluginsToAdd.push(plugin); } else { this.addPlugin(plugin); @@ -381,7 +375,11 @@ export class SegmentClient { process(incomingEvent: SegmentEvent) { const event = applyRawEventData(incomingEvent, this.store.userInfo.get()); - this.timeline.process(event); + if (this.store.isReady.get() === true) { + this.timeline.process(event); + } else { + this.pendingEvents.push(event); + } } private async trackDeepLinks() { @@ -399,29 +397,34 @@ export class SegmentClient { } } - private unsubscribeReadinessWatcher() { - this.readinessWatcher?.(); - } - + /** + * Executes when the state store is initialized. + * @param isReady + */ private onStorageReady(isReady: boolean) { - if (isReady && this.pluginsToAdd.length > 0 && !this.isAddingPlugins) { - this.isAddingPlugins = true; - try { - // start by adding the plugins - this.pluginsToAdd.forEach((plugin) => { - this.addPlugin(plugin); - }); - - // now that they're all added, clear the cache - // this prevents this block running for every update - this.pluginsToAdd = []; + if (isReady) { + // Add all plugins awaiting store + if (this.pluginsToAdd.length > 0 && !this.isAddingPlugins) { + this.isAddingPlugins = true; + try { + // start by adding the plugins + this.pluginsToAdd.forEach((plugin) => { + this.addPlugin(plugin); + }); + + // now that they're all added, clear the cache + // this prevents this block running for every update + this.pluginsToAdd = []; + } finally { + this.isAddingPlugins = false; + } + } - // finally set the flag which means plugins will be added + registered immediately in future - this.isStorageReady = true; - this.unsubscribeReadinessWatcher(); - } finally { - this.isAddingPlugins = false; + // Send all events in the queue + for (const e of this.pendingEvents) { + this.timeline.process(e); } + this.pendingEvents = []; } } diff --git a/packages/core/src/storage/sovranStorage.ts b/packages/core/src/storage/sovranStorage.ts index 02cafed9..ca3071a0 100644 --- a/packages/core/src/storage/sovranStorage.ts +++ b/packages/core/src/storage/sovranStorage.ts @@ -31,8 +31,13 @@ const INITIAL_VALUES: Data = { }, }; +interface ReadinessStore { + hasLoadedContext: boolean; +} + export class SovranStorage implements Storage { private storeId: string; + private readinessStore: Store; private contextStore: Store<{ context: DeepPartial }>; private settingsStore: Store<{ settings: SegmentAPIIntegrations }>; private eventsStore: Store<{ events: SegmentEvent[] }>; @@ -40,6 +45,9 @@ export class SovranStorage implements Storage { constructor(storeId: string) { this.storeId = storeId; + this.readinessStore = createStore({ + hasLoadedContext: false, + }); this.contextStore = createStore( { context: INITIAL_VALUES.context }, { @@ -68,6 +76,17 @@ export class SovranStorage implements Storage { ); this.fixAnonymousId(); + + // Wait for context to be loaded + const unsubscribeContext = this.contextStore.subscribe((store) => { + if (store.context !== INITIAL_VALUES.context) { + this.readinessStore.dispatch((state) => ({ + ...state, + hasLoadedContext: true, + })); + unsubscribeContext(); + } + }); } /** @@ -86,11 +105,18 @@ export class SovranStorage implements Storage { }); }; + // Check for all things that need to be ready before sending events through the timeline readonly isReady = { - get: () => true, - onChange: (_callback: (value: boolean) => void) => { - // No need to do anything since storage is always ready - return () => {}; + get: () => { + const ready = this.readinessStore.getState(); + return ready.hasLoadedContext; + }, + onChange: (callback: (value: boolean) => void) => { + return this.readinessStore.subscribe((store) => { + if (store.hasLoadedContext) { + callback(true); + } + }); }, };