Skip to content

fix: fixes missing context in first launch event after install #451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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