Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- feat: iOS surveys use the new response question id format ([#383](https://github.com/PostHog/posthog-ios/pull/383))

## 3.31.0 - 2025-08-29

- feat: surveys GA ([#381](https://github.com/PostHog/posthog-ios/pull/381))
Expand Down
4 changes: 4 additions & 0 deletions PostHog/Models/Surveys/PostHogSurvey+Display.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
switch self {
case let .open(question):
return PostHogDisplayOpenQuestion(
id: question.id,
question: question.question,
questionDescription: question.description,
questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(),
Expand All @@ -28,6 +29,7 @@

case let .link(question):
return PostHogDisplayLinkQuestion(
id: question.id,
question: question.question,
questionDescription: question.description,
questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(),
Expand All @@ -38,6 +40,7 @@

case let .rating(question):
return PostHogDisplayRatingQuestion(
id: question.id,
question: question.question,
questionDescription: question.description,
questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(),
Expand All @@ -52,6 +55,7 @@

case let .singleChoice(question), let .multipleChoice(question):
return PostHogDisplayChoiceQuestion(
id: question.id,
question: question.question,
questionDescription: question.description,
questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(),
Expand Down
10 changes: 10 additions & 0 deletions PostHog/Models/Surveys/PostHogSurveyQuestion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Foundation

/// Protocol defining common properties for all survey question types
protocol PostHogSurveyQuestionProperties {
/// Question ID, empty if none
var id: String { get }
/// Question text
var question: String { get }
/// Additional description or instructions (optional)
Expand Down Expand Up @@ -56,6 +58,10 @@ enum PostHogSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {
}
}

var id: String {
wrappedQuestion?.id ?? ""
}

var question: String {
wrappedQuestion?.question ?? ""
}
Expand Down Expand Up @@ -102,6 +108,7 @@ enum PostHogSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {

/// Represents a basic open-ended survey question
struct PostHogOpenSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {
let id: String
let question: String
let description: String?
let descriptionContentType: PostHogSurveyTextContentType?
Expand All @@ -113,6 +120,7 @@ struct PostHogOpenSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {

/// Represents a survey question with an associated link
struct PostHogLinkSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {
let id: String
let question: String
let description: String?
let descriptionContentType: PostHogSurveyTextContentType?
Expand All @@ -126,6 +134,7 @@ struct PostHogLinkSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {

/// Represents a rating-based survey question
struct PostHogRatingSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {
let id: String
let question: String
let description: String?
let descriptionContentType: PostHogSurveyTextContentType?
Expand All @@ -143,6 +152,7 @@ struct PostHogRatingSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {

/// Represents a multiple-choice or single-choice survey question
struct PostHogMultipleSurveyQuestion: PostHogSurveyQuestionProperties, Decodable {
let id: String
let question: String
let description: String?
let descriptionContentType: PostHogSurveyTextContentType?
Expand Down
10 changes: 10 additions & 0 deletions PostHog/Surveys/Models/PostHogDisplaySurveyQuestion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Foundation

/// Base class for all survey question types
@objc public class PostHogDisplaySurveyQuestion: NSObject {
/// The question ID, empty if none
@objc public let id: String
/// The main question text to display
@objc public let question: String
/// Optional additional description or context for the question
Expand All @@ -21,12 +23,14 @@ import Foundation
@objc public let buttonText: String?

init(
id: String,
question: String,
questionDescription: String?,
questionDescriptionContentType: PostHogDisplaySurveyTextContentType?,
isOptional: Bool,
buttonText: String?
) {
self.id = id
self.question = question
self.questionDescription = questionDescription
self.questionDescriptionContentType = questionDescriptionContentType ?? .text
Expand All @@ -45,6 +49,7 @@ import Foundation
public let link: String?

init(
id: String,
question: String,
questionDescription: String?,
questionDescriptionContentType: PostHogDisplaySurveyTextContentType?,
Expand All @@ -54,6 +59,7 @@ import Foundation
) {
self.link = link
super.init(
id: id,
question: question,
questionDescription: questionDescription,
questionDescriptionContentType: questionDescriptionContentType,
Expand All @@ -77,6 +83,7 @@ import Foundation
public let upperBoundLabel: String

init(
id: String,
question: String,
questionDescription: String?,
questionDescriptionContentType: PostHogDisplaySurveyTextContentType?,
Expand All @@ -94,6 +101,7 @@ import Foundation
self.lowerBoundLabel = lowerBoundLabel
self.upperBoundLabel = upperBoundLabel
super.init(
id: id,
question: question,
questionDescription: questionDescription,
questionDescriptionContentType: questionDescriptionContentType,
Expand All @@ -115,6 +123,7 @@ import Foundation
public let isMultipleChoice: Bool

init(
id: String,
question: String,
questionDescription: String?,
questionDescriptionContentType: PostHogDisplaySurveyTextContentType?,
Expand All @@ -130,6 +139,7 @@ import Foundation
self.shuffleOptions = shuffleOptions
self.isMultipleChoice = isMultipleChoice
super.init(
id: id,
question: question,
questionDescription: questionDescription,
questionDescriptionContentType: questionDescriptionContentType,
Expand Down
59 changes: 47 additions & 12 deletions PostHog/Surveys/PostHogSurveyIntegration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,16 @@
return nil
}

// TODO: ideally the handleSurveyResponse should pass the question ID as param but it would break the Flutter SDK for older versions
let questionId: String
if index < survey.questions.count {
let question = survey.questions[index]
questionId = question.id
} else {
// this should not happen, its only for back compatibility
questionId = ""
}

// 2. Get next step
let nextStep = getNextSurveyStep(
survey: activeSurvey,
Expand All @@ -419,7 +429,7 @@
)

// update response, next question index and survey completion
let allResponses = setActiveSurveyResponse(index: index, response: response, nextQuestion: nextSurveyQuestion)
let allResponses = setActiveSurveyResponse(id: questionId, index: index, response: response, nextQuestion: nextSurveyQuestion)

// send event if needed
// TODO: Partial responses
Expand Down Expand Up @@ -470,13 +480,6 @@
/// - survey: The completed survey
/// - responses: Dictionary of collected responses for each question
private func sendSurveySentEvent(survey: PostHogSurvey, responses: [String: PostHogSurveyResponse]) {
let questionProperties: [String: Any] = [
"$survey_questions": survey.questions.map(\.question),
"$set": [getSurveyInteractionProperty(survey: survey, property: "responded"): true],
]

// TODO: Should be doing some validation before sending the event?

let responsesProperties: [String: Any] = responses.compactMapValues { resp in
switch resp.type {
case .link: resp.linkClicked == true ? "link clicked" : nil
Expand All @@ -487,6 +490,27 @@
}
}

let surveyQuestions = survey.questions.enumerated().map { index, question in
let responseKey = question.id.isEmpty ? getOldResponseKey(for: index) : getNewResponseKey(for: question.id)
var questionData: [String: Any] = [
"id": question.id,
"question": question.question,
]

if let response = responsesProperties[responseKey] {
questionData["response"] = response
}

return questionData
}

let questionProperties: [String: Any] = [
"$survey_questions": surveyQuestions,
"$set": [getSurveyInteractionProperty(survey: survey, property: "responded"): true],
]

// TODO: Should be doing some validation before sending the event?

let additionalProperties = questionProperties.merging(responsesProperties, uniquingKeysWith: { _, new in new })

sendSurveyEvent(
Expand All @@ -499,7 +523,6 @@
/// Sends a `survey dismissed` event to PostHog instance
private func sendSurveyDismissedEvent(survey: PostHogSurvey) {
let additionalProperties: [String: Any] = [
"$survey_questions": survey.questions.map(\.question),
Copy link
Member Author

Choose a reason for hiding this comment

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

survey dismissed should not have $survey_questions

Choose a reason for hiding this comment

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

in the posthog-js, it also includes the $survey_questions property.
it's useful for getting all responses up until when the user dismissed it

Copy link
Member Author

Choose a reason for hiding this comment

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

mmm i dont follow because our APIs only query for survey sent events when getting responses, how is that useful?

Copy link
Contributor

Choose a reason for hiding this comment

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

if I recall correctly, I copied over from what posthog-js ws doing, thinking that it's part of the partial response feature (was wip back then if I remember correctly), but good point on APIs above. Let's wait for @lucasheriques to comment. If we are not using this anywhere, we can drop from all sdks?

Choose a reason for hiding this comment

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

I think makes sense, we can drop it. When we implement partial responses for mobile SDKs (sometime in the future) we'll have access to all responses regardless. Will remove from the JS sdk too

"$set": [
getSurveyInteractionProperty(survey: survey, property: "dismissed"): true,
],
Expand Down Expand Up @@ -567,16 +590,23 @@

/// Stores a response for the current question in the active survey, and returns updated responses
/// - Parameters:
/// - id: The question ID, empty if none
/// - index: The index of the question being answered
/// - response: The user's response to store
/// - nextQuestion: The next question index and completion info
private func setActiveSurveyResponse(
id: String,
index: Int,
response: PostHogSurveyResponse,
nextQuestion: PostHogNextSurveyQuestion
) -> [String: PostHogSurveyResponse] {
activeSurveyLock.withLock {
activeSurveyResponses[getResponseKey(for: index)] = response
// keeping the old response key format for back compatibility
activeSurveyResponses[getOldResponseKey(for: index)] = response
if !id.isEmpty {
// setting the new response key format
activeSurveyResponses[getNewResponseKey(for: id)] = response
}
activeSurveyQuestionIndex = nextQuestion.questionIndex
activeSurveyCompleted = nextQuestion.isSurveyCompleted
return activeSurveyResponses
Expand Down Expand Up @@ -728,11 +758,16 @@
}
}

// Returns the survey response key for a specific question index
private func getResponseKey(for index: Int) -> String {
// Returns the old survey response key for a specific question index
private func getOldResponseKey(for index: Int) -> String {
index == 0 ? kSurveyResponseKey : "\(kSurveyResponseKey)_\(index)"
}

// Returns the new survey response key for a specific question id
private func getNewResponseKey(for questionId: String) -> String {
"\(kSurveyResponseKey)_\(questionId)"
}

func canShowNextSurvey() -> Bool {
activeSurveyLock.withLock { activeSurvey == nil }
}
Expand Down
5 changes: 5 additions & 0 deletions PostHogTests/PostHogSurveysTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum PostHogSurveysTest {
#expect(sut.questions.count == 1)

if case let .open(question) = sut.questions[0] {
#expect(question.id == "300")
#expect(question.question == "What would you like to see in our Core Web Vitals product?")
#expect(question.description == "Core Web Vitals just launched and we're interested in hearing (reading?) from you on what you think we're missing with this product")
#expect(question.originalQuestionIndex == 0)
Expand Down Expand Up @@ -1360,6 +1361,7 @@ private extension PostHogSurvey {
questions: [PostHogSurveyQuestion] = [
.open(
PostHogOpenSurveyQuestion(
id: "",
question: "Some question",
description: "Some description",
descriptionContentType: nil,
Expand Down Expand Up @@ -1406,6 +1408,7 @@ private extension PostHogOpenSurveyQuestion {
branching: PostHogSurveyQuestionBranching? = nil
) -> PostHogOpenSurveyQuestion {
PostHogOpenSurveyQuestion(
id: "",
question: question,
description: "",
descriptionContentType: nil,
Expand All @@ -1424,6 +1427,7 @@ private extension PostHogMultipleSurveyQuestion {
branching: PostHogSurveyQuestionBranching? = nil
) -> PostHogMultipleSurveyQuestion {
PostHogMultipleSurveyQuestion(
id: "",
question: question,
description: "",
descriptionContentType: nil,
Expand All @@ -1446,6 +1450,7 @@ private extension PostHogRatingSurveyQuestion {
branching: PostHogSurveyQuestionBranching? = nil
) -> PostHogRatingSurveyQuestion {
PostHogRatingSurveyQuestion(
id: "",
question: question,
description: "",
descriptionContentType: nil,
Expand Down
Loading
Loading