Skip to content
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

Add survey support for React Native #333

Merged
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
24 changes: 24 additions & 0 deletions posthog-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * as utils from './utils'
import { LZString } from './lz-string'
import { SimpleEventEmitter } from './eventemitter'
import { uuidv7 } from './vendor/uuidv7'
import { Survey, SurveyResponse } from './posthog-surveys-types'

class PostHogFetchHttpError extends Error {
name = 'PostHogFetchHttpError'
Expand Down Expand Up @@ -472,6 +473,29 @@ export abstract class PostHogCoreStateless {
}
}

/***
*** SURVEYS
***/

public async getSurveys(): Promise<Survey[]> {
await this._initPromise

const url = `${this.host}/api/surveys/?token=${this.apiKey}`
const fetchOptions: PostHogFetchOptions = {
method: 'GET',
headers: this.getCustomHeaders(),
}

const response = await this.fetchWithRetry(url, fetchOptions)
.then((response) => response.json() as Promise<SurveyResponse>)
.catch((error) => {
this._events.emit('error', error)
return { surveys: [] }
})
Comment on lines +489 to +494
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: error handling returns empty surveys array but doesn't retry failed requests like other API calls. Consider using fetchWithRetry with retry options for consistency.


return response.surveys
}

/***
*** QUEUEING AND FLUSHING
***/
Expand Down
199 changes: 199 additions & 0 deletions posthog-core/src/posthog-surveys-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* Having Survey types in types.ts was confusing tsc
* and generating an invalid module.d.ts
* See https://github.com/PostHog/posthog-js/issues/698
*/

export interface SurveyAppearance {
// keep in sync with frontend/src/types.ts -> SurveyAppearance
backgroundColor?: string
submitButtonColor?: string
// deprecate submit button text eventually
submitButtonText?: string
submitButtonTextColor?: string
ratingButtonColor?: string
ratingButtonActiveColor?: string
autoDisappear?: boolean
displayThankYouMessage?: boolean
thankYouMessageHeader?: string
thankYouMessageDescription?: string
thankYouMessageDescriptionContentType?: SurveyQuestionDescriptionContentType
thankYouMessageCloseButtonText?: string
borderColor?: string
position?: 'left' | 'right' | 'center'
placeholder?: string
shuffleQuestions?: boolean
surveyPopupDelaySeconds?: number
// widget options
widgetType?: 'button' | 'tab' | 'selector'
widgetSelector?: string
widgetLabel?: string
widgetColor?: string
}

export enum SurveyType {
Popover = 'popover',
API = 'api',
Widget = 'widget',
}

export type SurveyQuestion = BasicSurveyQuestion | LinkSurveyQuestion | RatingSurveyQuestion | MultipleSurveyQuestion

export type SurveyQuestionDescriptionContentType = 'html' | 'text'

interface SurveyQuestionBase {
question: string
description?: string | null
descriptionContentType?: SurveyQuestionDescriptionContentType
optional?: boolean
buttonText?: string
originalQuestionIndex: number
branching?: NextQuestionBranching | EndBranching | ResponseBasedBranching | SpecificQuestionBranching
}

export interface BasicSurveyQuestion extends SurveyQuestionBase {
type: SurveyQuestionType.Open
}

export interface LinkSurveyQuestion extends SurveyQuestionBase {
type: SurveyQuestionType.Link
link?: string | null
}

export interface RatingSurveyQuestion extends SurveyQuestionBase {
type: SurveyQuestionType.Rating
display: 'number' | 'emoji'
scale: 3 | 5 | 7 | 10
lowerBoundLabel: string
upperBoundLabel: string
}

export interface MultipleSurveyQuestion extends SurveyQuestionBase {
type: SurveyQuestionType.SingleChoice | SurveyQuestionType.MultipleChoice
choices: string[]
hasOpenChoice?: boolean
shuffleOptions?: boolean
}

export enum SurveyQuestionType {
Open = 'open',
MultipleChoice = 'multiple_choice',
SingleChoice = 'single_choice',
Rating = 'rating',
Link = 'link',
}

export enum SurveyQuestionBranchingType {
NextQuestion = 'next_question',
End = 'end',
ResponseBased = 'response_based',
SpecificQuestion = 'specific_question',
}

interface NextQuestionBranching {
type: SurveyQuestionBranchingType.NextQuestion
}

interface EndBranching {
type: SurveyQuestionBranchingType.End
}

interface ResponseBasedBranching {
type: SurveyQuestionBranchingType.ResponseBased
responseValues: Record<string, any>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: responseValues type is too permissive with Record<string, any>. Consider using a more specific type to ensure type safety

}

interface SpecificQuestionBranching {
type: SurveyQuestionBranchingType.SpecificQuestion
index: number
}

export interface SurveyResponse {
surveys: Survey[]
}

export type SurveyCallback = (surveys: Survey[]) => void

export type SurveyUrlMatchType = 'regex' | 'not_regex' | 'exact' | 'is_not' | 'icontains' | 'not_icontains'

export interface SurveyElement {
text?: string
$el_text?: string
tag_name?: string
href?: string
attr_id?: string
attr_class?: string[]
nth_child?: number
nth_of_type?: number
attributes?: Record<string, any>
event_id?: number
order?: number
group_id?: number
}
export interface SurveyRenderReason {
visible: boolean
disabledReason?: string
}

export interface Survey {
// Sync this with the backend's SurveyAPISerializer!
id: string
name: string
description: string
type: SurveyType
feature_flag_keys:
| {
key: string
value?: string
}[]
| null
linked_flag_key: string | null
targeting_flag_key: string | null
internal_targeting_flag_key: string | null
questions: SurveyQuestion[]
appearance: SurveyAppearance | null
conditions: {
url?: string
selector?: string
seenSurveyWaitPeriodInDays?: number
urlMatchType?: SurveyUrlMatchType
events: {
repeatedActivation?: boolean
values: {
name: string
}[]
} | null
actions: {
values: SurveyActionType[]
} | null
} | null
start_date: string | null
end_date: string | null
current_iteration: number | null
current_iteration_start_date: string | null
}

export interface SurveyActionType {
id: number
name: string | null
steps?: ActionStepType[]
}

/** Sync with plugin-server/src/types.ts */
export type ActionStepStringMatching = 'contains' | 'exact' | 'regex'

export interface ActionStepType {
event?: string | null
selector?: string | null
/** @deprecated Only `selector` should be used now. */
tag_name?: string
text?: string | null
/** @default StringMatching.Exact */
text_matching?: ActionStepStringMatching | null
href?: string | null
/** @default ActionStepStringMatching.Exact */
href_matching?: ActionStepStringMatching | null
Comment on lines +191 to +195
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: text_matching and href_matching have default values in comments but these aren't enforced by types. Consider using default type parameters

url?: string | null
/** @default StringMatching.Contains */
url_matching?: ActionStepStringMatching | null
}
2 changes: 2 additions & 0 deletions posthog-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export enum PostHogPersistedProperty {
InstalledAppVersion = 'installed_app_version', // only used by posthog-react-native
SessionReplay = 'session_replay', // only used by posthog-react-native
DecideEndpointWasHit = 'decide_endpoint_was_hit', // only used by posthog-react-native
SurveyLastSeenDate = 'survey_last_seen_date', // only used by posthog-react-native
SurveysSeen = 'surveys_seen', // only used by posthog-react-native
}

export type PostHogFetchOptions = {
Expand Down
1 change: 1 addition & 0 deletions posthog-react-native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './src/hooks/useFeatureFlag'
export * from './src/hooks/usePostHog'
export * from './src/PostHogProvider'
export * from './src/types'
export * from './src/surveys'
6 changes: 5 additions & 1 deletion posthog-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"react-native": "^0.69.1",
"react-native-device-info": "^10.3.0",
"react-native-navigation": "^6.0.0",
"react-native-safe-area-context": ">= 4.10.1",
"react-native-svg": "^15.2.0",
"react-native-localize": "^3.0.0",
"posthog-react-native-session-replay": "^1.0.0"
},
Expand All @@ -45,6 +47,8 @@
"expo-localization": ">= 11.0.0",
"react-native-device-info": ">= 10.0.0",
"react-native-navigation": ">=6.0.0",
"react-native-safe-area-context": ">= 4.10.1",
"react-native-svg": ">= 15.2.0",
"posthog-react-native-session-replay": "^1.0.0"
},
Comment on lines 52 to 53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: posthog-react-native-session-replay is listed as a peer dependency but not marked as optional in peerDependenciesMeta

Suggested change
"posthog-react-native-session-replay": "^1.0.0"
},
"posthog-react-native-session-replay": "^1.0.0"
},
"peerDependenciesMeta": {
"posthog-react-native-session-replay": {
"optional": true
},

"peerDependenciesMeta": {
Expand Down Expand Up @@ -72,7 +76,7 @@
"react-native-navigation": {
"optional": true
},
"posthog-react-native-session-replay": {
"react-native-safe-area-context": {
"optional": true
}
}
Comment on lines +79 to 82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: react-native-svg is missing from peerDependenciesMeta but is listed as an optional peer dependency. Add it to maintain consistency

Suggested change
"react-native-safe-area-context": {
"optional": true
}
}
"react-native-safe-area-context": {
"optional": true
},
"react-native-svg": {
"optional": true
}
}

Expand Down
22 changes: 22 additions & 0 deletions posthog-react-native/src/optional/OptionalReactNativeSafeArea.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type RNSafeAreaContext from 'react-native-safe-area-context'

let OptionalRNSafeArea: typeof RNSafeAreaContext | undefined = undefined

try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
OptionalRNSafeArea = require('react-native-safe-area-context')
} catch (e) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: empty catch block silently ignores errors that could be useful for debugging


function createDefaultInsets(): RNSafeAreaContext.EdgeInsets {
// If the library isn't available, fall back to a default which should cover most devices
return { top: 60, bottom: 30, left: 0, right: 0 }
}

export const useOptionalSafeAreaInsets = (): RNSafeAreaContext.EdgeInsets => {
const useSafeAreaInsets = OptionalRNSafeArea?.useSafeAreaInsets ?? createDefaultInsets
try {
return useSafeAreaInsets()
} catch (err) {
return createDefaultInsets()
}
}
1 change: 1 addition & 0 deletions posthog-react-native/src/posthog-rn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from './types'
import { withReactNativeNavigation } from './frameworks/wix-navigation'
import { OptionalReactNativeSessionReplay } from './optional/OptionalSessionReplay'
import { Survey, SurveyResponse } from '../../posthog-core/src/posthog-surveys-types'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO Remove this import now the fetch method has moved to core


export type PostHogOptions = PostHogCoreOptions & {
/** Allows you to provide the storage type. By default 'file'.
Expand Down
Loading