Skip to content

Commit

Permalink
fix: fixes missing context in first launch event after install (#451)
Browse files Browse the repository at this point in the history
* fix: fixes missing context in first launch event after install

* fix: fixing detox test, refactoring a single callback for onStoreReady
  • Loading branch information
oscb authored Feb 23, 2022
1 parent 0a51559 commit efb2805
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 45 deletions.
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
77 changes: 40 additions & 37 deletions packages/core/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ export class SegmentClient {
// internal time to know when to flush, ticks every second
private flushInterval: ReturnType<typeof setInterval> | null = null;

// Watcher for isReady updates to the storage
private readinessWatcher?: Unsubscribe = undefined;

// unsubscribe watchers for the store
private watchers: Unsubscribe[] = [];

Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }));
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -290,7 +285,6 @@ export class SegmentClient {
clearInterval(this.flushInterval);
}

this.unsubscribeReadinessWatcher();
this.unsubscribeStorageWatchers();

this.appStateSubscription?.remove();
Expand Down Expand Up @@ -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);
Expand All @@ -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() {
Expand All @@ -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 = [];
}
}

Expand Down
34 changes: 30 additions & 4 deletions packages/core/src/storage/sovranStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,23 @@ const INITIAL_VALUES: Data = {
},
};

interface ReadinessStore {
hasLoadedContext: boolean;
}

export class SovranStorage implements Storage {
private storeId: string;
private readinessStore: Store<ReadinessStore>;
private contextStore: Store<{ context: DeepPartial<Context> }>;
private settingsStore: Store<{ settings: SegmentAPIIntegrations }>;
private eventsStore: Store<{ events: SegmentEvent[] }>;
private userInfoStore: Store<{ userInfo: UserInfoState }>;

constructor(storeId: string) {
this.storeId = storeId;
this.readinessStore = createStore<ReadinessStore>({
hasLoadedContext: false,
});
this.contextStore = createStore(
{ context: INITIAL_VALUES.context },
{
Expand Down Expand Up @@ -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();
}
});
}

/**
Expand All @@ -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);
}
});
},
};

Expand Down

0 comments on commit efb2805

Please sign in to comment.