Skip to content

Commit ab30675

Browse files
authored
fix: surveys displayed only from cache (#2415)
* feat: ensure surveys are displayed on initial app launch # Conflicts: # packages/react-native/src/posthog-rn.ts * chore: add changeset * fix: make onSurveysReady private * fix: survey length check * fix: dont clear survey cache * fix: make onSurveysReady internal * fix: add config parameter to flagsAsync * fix: prevent surveys provider from hanging on initialization errors * fix: add log * fix: use _logger
1 parent 217fd1d commit ab30675

File tree

6 files changed

+176
-19
lines changed

6 files changed

+176
-19
lines changed

.changeset/warm-stars-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'posthog-react-native': patch
3+
---
4+
5+
fix surveys only appear on subsequent app launches after being loaded and cached

packages/core/src/posthog-core-stateless.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,11 +450,13 @@ export abstract class PostHogCoreStateless {
450450
groups: Record<string, string | number> = {},
451451
personProperties: Record<string, string> = {},
452452
groupProperties: Record<string, Record<string, string>> = {},
453-
extraPayload: Record<string, any> = {}
453+
extraPayload: Record<string, any> = {},
454+
fetchConfig: boolean = true
454455
): Promise<PostHogFlagsResponse | undefined> {
455456
await this._initPromise
456457

457-
const url = `${this.host}/flags/?v=2&config=true`
458+
const configParam = fetchConfig ? '&config=true' : ''
459+
const url = `${this.host}/flags/?v=2${configParam}`
458460
const requestData: Record<string, any> = {
459461
token: this.apiKey,
460462
distinct_id: distinctId,

packages/core/src/posthog-core.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,12 +450,15 @@ export abstract class PostHogCore extends PostHogCoreStateless {
450450
/***
451451
*** FEATURE FLAGS
452452
***/
453-
private async flagsAsync(sendAnonDistinctId: boolean = true): Promise<PostHogFlagsResponse | undefined> {
453+
protected async flagsAsync(
454+
sendAnonDistinctId: boolean = true,
455+
fetchConfig: boolean = true
456+
): Promise<PostHogFlagsResponse | undefined> {
454457
await this._initPromise
455458
if (this._flagsResponsePromise) {
456459
return this._flagsResponsePromise
457460
}
458-
return this._flagsAsync(sendAnonDistinctId)
461+
return this._flagsAsync(sendAnonDistinctId, fetchConfig)
459462
}
460463

461464
private cacheSessionReplay(source: string, response?: PostHogRemoteConfig): void {
@@ -545,7 +548,10 @@ export abstract class PostHogCore extends PostHogCoreStateless {
545548
return this._remoteConfigResponsePromise
546549
}
547550

548-
private async _flagsAsync(sendAnonDistinctId: boolean = true): Promise<PostHogFlagsResponse | undefined> {
551+
private async _flagsAsync(
552+
sendAnonDistinctId: boolean = true,
553+
fetchConfig: boolean = true
554+
): Promise<PostHogFlagsResponse | undefined> {
549555
this._flagsResponsePromise = this._initPromise
550556
.then(async () => {
551557
const distinctId = this.getDistinctId()
@@ -565,7 +571,8 @@ export abstract class PostHogCore extends PostHogCoreStateless {
565571
groups as PostHogGroupProperties,
566572
personProperties,
567573
groupProperties,
568-
extraProperties
574+
extraProperties,
575+
fetchConfig
569576
)
570577
// Add check for quota limitation on feature flags
571578
if (res?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {

packages/core/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export type PostHogRemoteConfig = {
173173

174174
export type FeatureFlagValue = string | boolean
175175

176-
export type PostHogFlagsResponse = Omit<PostHogRemoteConfig, 'surveys' | 'hasFeatureFlags'> & {
176+
export type PostHogFlagsResponse = Omit<PostHogRemoteConfig, 'hasFeatureFlags'> & {
177177
featureFlags: {
178178
[key: string]: FeatureFlagValue
179179
}

packages/react-native/src/posthog-rn.ts

Lines changed: 154 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
PostHogFetchOptions,
1010
PostHogFetchResponse,
1111
PostHogPersistedProperty,
12+
Survey,
1213
SurveyResponse,
1314
logFlushError,
1415
maybeAdd,
@@ -85,6 +86,8 @@ export class PostHog extends PostHogCore {
8586
private _disableSurveys: boolean
8687
private _disableRemoteConfig: boolean
8788
private _errorTracking: ErrorTracking
89+
private _surveysReadyPromise: Promise<void> | null = null
90+
private _surveysReady: boolean = false
8891

8992
/**
9093
* Creates a new PostHog instance for React Native. You can find all configuration options in the [React Native SDK docs](https://posthog.com/docs/libraries/react-native#configuration-options).
@@ -180,13 +183,41 @@ export class PostHog extends PostHogCore {
180183

181184
if (this._disableRemoteConfig === false) {
182185
this.reloadRemoteConfigAsync()
186+
.then((response) => {
187+
if (response) {
188+
this._handleSurveysFromRemoteConfig(response)
189+
}
190+
})
191+
.catch((error) => {
192+
this._logger.error('Error loading remote config:', error)
193+
})
194+
.finally(() => {
195+
this._notifySurveysReady()
196+
})
183197
} else {
184198
this._logger.info('Remote config is disabled.')
199+
185200
if (options?.preloadFeatureFlags !== false) {
186201
this._logger.info('Feature flags will be preloaded from Flags API.')
187-
this.reloadFeatureFlags()
202+
// Preload flags (and parse surveys as well since we are calling with config=true already)
203+
this._flagsAsyncWithSurveys()
204+
.catch((error) => {
205+
this._logger.error('Error loading flags with surveys:', error)
206+
})
207+
.finally(() => {
208+
this._notifySurveysReady()
209+
})
188210
} else {
189-
this._logger.info('preloadFeatureFlags is disabled.')
211+
this._logger.info('preloadFeatureFlags is disabled, loading surveys from API.')
212+
// Load surveys directly from API since both remote config and preloading feature flags are disabled
213+
// Note: if flags are not loaded/cached then surveys will not be displayed until reloadFeatureFlags() is called, since surveys depend on internal flags
214+
this._loadSurveysFromAPI()
215+
.catch((error) => {
216+
this._logger.error('Error loading surveys from API:', error)
217+
})
218+
.finally(() => {
219+
this._notifySurveysReady()
220+
})
190221
}
191222
}
192223

@@ -846,7 +877,7 @@ export class PostHog extends PostHogCore {
846877
public async getSurveys(): Promise<SurveyResponse['surveys']> {
847878
if (this._disableSurveys === true) {
848879
this._logger.info('Loading surveys is disabled.')
849-
this.setPersistedProperty<SurveyResponse['surveys']>(PostHogPersistedProperty.Surveys, null)
880+
this._cacheSurveys(null, 'disabled in config')
850881
return []
851882
}
852883

@@ -855,20 +886,133 @@ export class PostHog extends PostHogCore {
855886
if (surveys && surveys.length > 0) {
856887
this._logger.info('Surveys fetched from storage: ', JSON.stringify(surveys))
857888
return surveys
889+
}
890+
891+
this._logger.info('No surveys found in storage')
892+
return []
893+
}
894+
895+
/**
896+
* Returns a promise that resolves when surveys are ready to be loaded.
897+
* If surveys are already loaded and ready to go, returns a resolved promise instead.
898+
* @internal
899+
*/
900+
_onSurveysReady(): Promise<void> {
901+
if (this._surveysReady) {
902+
// If surveys are already ready, resolve immediately
903+
return Promise.resolve()
904+
}
905+
906+
if (!this._surveysReadyPromise) {
907+
this._surveysReadyPromise = new Promise<void>((resolve) => {
908+
this._surveysReadyResolve = resolve
909+
})
910+
}
911+
912+
return this._surveysReadyPromise
913+
}
914+
915+
private _surveysReadyResolve: (() => void) | null = null
916+
917+
/**
918+
* Helper function to cache surveys to storage with consistent logging
919+
*/
920+
private _cacheSurveys(surveys: Survey[] | null, source: string): void {
921+
this.setPersistedProperty<SurveyResponse['surveys']>(PostHogPersistedProperty.Surveys, surveys)
922+
923+
if (surveys && surveys.length > 0) {
924+
this._logger.info(`Surveys cached from ${source}:`, JSON.stringify(surveys))
925+
} else if (surveys === null) {
926+
this._logger.info(`Surveys cleared (${source})`)
858927
} else {
859-
this._logger.info('No surveys found in storage')
928+
this._logger.info(`No surveys to cache from ${source})`)
860929
}
930+
}
861931

862-
if (this._disableRemoteConfig === true) {
863-
const surveysFromApi = await super.getSurveysStateless()
932+
/**
933+
* Internal method to notify that surveys are ready
934+
*/
935+
private _notifySurveysReady(): void {
936+
this._surveysReady = true
937+
if (this._surveysReadyResolve) {
938+
this._surveysReadyResolve()
939+
this._surveysReadyResolve = null
940+
this._surveysReadyPromise = null
941+
}
942+
}
864943

865-
if (surveysFromApi && surveysFromApi.length > 0) {
866-
this.setPersistedProperty<SurveyResponse['surveys']>(PostHogPersistedProperty.Surveys, surveysFromApi)
867-
return surveysFromApi
944+
/**
945+
* Handle surveys from remote config response
946+
*/
947+
private _handleSurveysFromRemoteConfig(response: any): void {
948+
if (this._disableSurveys === true) {
949+
this._logger.info('Loading surveys skipped, disabled.')
950+
this._cacheSurveys(null, 'remote config (disabled)')
951+
return
952+
}
953+
954+
const surveys = response.surveys
955+
956+
// If surveys is not an array, it means there are no surveys (its a boolean)
957+
if (Array.isArray(surveys) && surveys.length > 0) {
958+
this._cacheSurveys(surveys as Survey[], 'remote config')
959+
} else {
960+
this._cacheSurveys(null, 'remote config')
961+
}
962+
}
963+
964+
/**
965+
* Load flags AND handle surveys from the flags response (only when remote config is disabled)
966+
*/
967+
private async _flagsAsyncWithSurveys(): Promise<void> {
968+
try {
969+
const flagsResponse = await this.flagsAsync(true, true)
970+
971+
// Only handle surveys from flags if remote config is disabled and surveys are enabled
972+
// When remote config is enabled, surveys will come from there instead
973+
if (this._disableRemoteConfig === true) {
974+
if (this._disableSurveys === true) {
975+
this._logger.info('Loading surveys skipped, disabled.')
976+
this._cacheSurveys(null, 'flags (disabled)')
977+
return
978+
}
979+
980+
// Handle surveys from the response (surveys key is included when config=true)
981+
const surveys = flagsResponse?.surveys
982+
983+
// If surveys is not an array, it means there are no surveys (its a boolean)
984+
if (Array.isArray(surveys) && surveys.length > 0) {
985+
this._cacheSurveys(surveys as Survey[], 'flags endpoint')
986+
} else {
987+
this._logger.info('No surveys in flags response')
988+
this._cacheSurveys(null, 'flags endpoint')
989+
}
868990
}
991+
} catch (error) {
992+
this._logger.error('Error in _flagsAsyncWithSurveys:', error)
869993
}
994+
}
870995

871-
return []
996+
/**
997+
* Internal method to load surveys from API (when remote config is disabled)
998+
*/
999+
private async _loadSurveysFromAPI(): Promise<void> {
1000+
if (this._disableSurveys === true) {
1001+
this._logger.info('Loading surveys skipped, disabled.')
1002+
this._cacheSurveys(null, 'API (disabled)')
1003+
return
1004+
}
1005+
1006+
try {
1007+
const surveysFromApi = await super.getSurveysStateless()
1008+
if (surveysFromApi && surveysFromApi.length > 0) {
1009+
this._cacheSurveys(surveysFromApi, 'API')
1010+
} else {
1011+
this._cacheSurveys(null, 'API')
1012+
}
1013+
} catch (error) {
1014+
this._logger.error('Error loading surveys from API:', error)
1015+
}
8721016
}
8731017

8741018
private async startSessionReplay(options?: PostHogOptions): Promise<void> {

packages/react-native/src/surveys/PostHogSurveyProvider.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,9 @@ export function PostHogSurveyProvider(props: PostHogSurveyProviderProps): JSX.El
8383

8484
// Load surveys once
8585
useEffect(() => {
86-
// TODO: for the first time, sometimes the surveys are not fetched from storage, so we need to fetch them from the API
87-
// because the remote config is still being fetched from the API
8886
posthog
8987
.ready()
88+
.then(() => posthog._onSurveysReady())
9089
.then(() => posthog.getSurveys())
9190
.then(setSurveys)
9291
.catch(() => {})

0 commit comments

Comments
 (0)