Skip to content

Commit 4a97ea8

Browse files
authored
Merge pull request #83 from PostHog/kefapps/kef-176-implement-project-scoped-posthog_survey-resource
feat: add project-scoped survey resource
2 parents 9a47600 + 2631510 commit 4a97ea8

11 files changed

Lines changed: 1762 additions & 0 deletions

File tree

docs/resources/survey.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "posthog_survey Resource - posthog"
4+
subcategory: ""
5+
description: |-
6+
Manage PostHog surveys via the project-scoped surveys API.
7+
---
8+
9+
# posthog_survey (Resource)
10+
11+
Manage PostHog surveys via the project-scoped surveys API.
12+
13+
## Example Usage
14+
15+
```terraform
16+
# NPS-style feedback survey delivered via the in-product popover.
17+
resource "posthog_survey" "nps" {
18+
name = "PostHog NPS"
19+
description = "Quarterly NPS survey for PostHog users."
20+
type = "popover"
21+
schedule = "once"
22+
23+
questions_json = jsonencode([
24+
{
25+
type = "rating"
26+
question = "How would you rate your PostHog experience?"
27+
scale = 10
28+
buttonText = "Submit"
29+
isNpsQuestion = true
30+
}
31+
])
32+
33+
conditions_json = jsonencode({
34+
url = "https://us.posthog.com/*"
35+
})
36+
}
37+
38+
# Open-ended follow-up triggered only for users on a specific feature flag.
39+
resource "posthog_survey" "feature_feedback" {
40+
name = "New onboarding feedback"
41+
type = "popover"
42+
schedule = "always"
43+
44+
# Show this survey to users in the variant of an existing flag.
45+
linked_flag_id = 12345
46+
47+
questions_json = jsonencode([
48+
{
49+
type = "open"
50+
question = "What was the most confusing part of the new onboarding?"
51+
description = "We read every response."
52+
}
53+
])
54+
55+
appearance_json = jsonencode({
56+
submitButtonText = "Send feedback"
57+
})
58+
}
59+
```
60+
61+
<!-- schema generated by tfplugindocs -->
62+
## Schema
63+
64+
### Required
65+
66+
- `name` (String) Survey name.
67+
- `questions_json` (String) JSON array describing the survey questions.
68+
- `type` (String) Survey type. Supported values are `popover`, `widget`, `external_survey`, and `api`.
69+
70+
### Optional
71+
72+
- `appearance_json` (String) JSON object describing survey appearance settings.
73+
- `archived` (Boolean) Whether the survey is archived.
74+
- `conditions_json` (String) JSON object describing survey display and targeting conditions.
75+
- `create_in_folder` (String) Folder identifier used only during survey creation.
76+
- `description` (String) Survey description.
77+
- `enable_iframe_embedding` (Boolean) Whether the survey is embeddable in an iframe.
78+
- `enable_partial_responses` (Boolean) Whether partial responses are stored when a respondent exits early.
79+
- `end_date` (String) RFC3339 end date for the survey.
80+
- `form_content_json` (String) JSON object describing custom form content.
81+
- `iteration_count` (Number) Number of survey recurrences when `schedule` is `recurring`.
82+
- `iteration_frequency_days` (Number) Number of days between recurrences when `schedule` is `recurring`.
83+
- `linked_flag_id` (Number) Feature flag ID linked to the survey. Remove this attribute to unlink the flag from the survey.
84+
- `linked_insight_id` (Number) Insight ID linked to the survey. This field is write-only in the PostHog API and is preserved from Terraform state when configured.
85+
- `project_id` (String) Project ID (environment) for this resource. Overrides the provider-level project_id.
86+
- `response_sampling_daily_limits_json` (String) JSON object describing daily response sampling limits.
87+
- `response_sampling_interval` (Number) Response sampling interval value.
88+
- `response_sampling_interval_type` (String) Response sampling interval type. Supported values are `day`, `week`, and `month`.
89+
- `response_sampling_limit` (Number) Maximum responses allowed during each sampling interval.
90+
- `response_sampling_start_date` (String) RFC3339 date when response sampling starts.
91+
- `responses_limit` (Number) Maximum number of responses allowed for the survey.
92+
- `schedule` (String) Survey schedule. Supported values are `once`, `recurring`, and `always`.
93+
- `start_date` (String) RFC3339 start date for the survey.
94+
- `targeting_flag_filters_json` (String) JSON object describing targeting flag filters. This input is write-only in the PostHog API and is preserved from Terraform state when configured.
95+
- `targeting_flag_id` (Number) Existing targeting feature flag ID to use for this survey. Remove this attribute to detach the targeting flag.
96+
- `translations_json` (String) JSON object describing translated survey content.
97+
98+
### Read-Only
99+
100+
- `created_at` (String) RFC3339 creation timestamp of the survey.
101+
- `created_by_json` (String) JSON object describing the survey creator.
102+
- `id` (String) UUID of the survey.
103+
- `internal_targeting_flag_json` (String) JSON object describing the internal targeting feature flag returned by the API.
104+
- `linked_flag_json` (String) JSON object describing the linked feature flag returned by the API.
105+
- `targeting_flag_json` (String) JSON object describing the targeting feature flag returned by the API.
106+
107+
## Import
108+
109+
Import is supported using the following syntax:
110+
111+
The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:
112+
113+
```shell
114+
terraform import posthog_survey.example "<project_id>/<survey_uuid>"
115+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
terraform import posthog_survey.example "<project_id>/<survey_uuid>"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# NPS-style feedback survey delivered via the in-product popover.
2+
resource "posthog_survey" "nps" {
3+
name = "PostHog NPS"
4+
description = "Quarterly NPS survey for PostHog users."
5+
type = "popover"
6+
schedule = "once"
7+
8+
questions_json = jsonencode([
9+
{
10+
type = "rating"
11+
question = "How would you rate your PostHog experience?"
12+
scale = 10
13+
buttonText = "Submit"
14+
isNpsQuestion = true
15+
}
16+
])
17+
18+
conditions_json = jsonencode({
19+
url = "https://us.posthog.com/*"
20+
})
21+
}
22+
23+
# Open-ended follow-up triggered only for users on a specific feature flag.
24+
resource "posthog_survey" "feature_feedback" {
25+
name = "New onboarding feedback"
26+
type = "popover"
27+
schedule = "always"
28+
29+
# Show this survey to users in the variant of an existing flag.
30+
linked_flag_id = 12345
31+
32+
questions_json = jsonencode([
33+
{
34+
type = "open"
35+
question = "What was the most confusing part of the new onboarding?"
36+
description = "We read every response."
37+
}
38+
])
39+
40+
appearance_json = jsonencode({
41+
submitButtonText = "Send feedback"
42+
})
43+
}

examples/surveys/main.tf

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
terraform {
2+
required_providers {
3+
posthog = {
4+
source = "posthog/posthog"
5+
}
6+
}
7+
}
8+
9+
provider "posthog" {}
10+
11+
resource "posthog_survey" "customer_feedback" {
12+
name = "Customer feedback survey"
13+
description = "Basic in-product feedback survey managed by Terraform"
14+
type = "popover"
15+
schedule = "once"
16+
17+
questions_json = jsonencode([
18+
{
19+
type = "rating"
20+
question = "How would you rate your PostHog experience?"
21+
scale = 10
22+
buttonText = "Submit"
23+
isNpsQuestion = true
24+
}
25+
])
26+
27+
conditions_json = jsonencode({
28+
url = "https://us.posthog.com/*"
29+
})
30+
}
31+
32+
output "survey_id" {
33+
description = "UUID of the survey"
34+
value = posthog_survey.customer_feedback.id
35+
}

internal/httpclient/survey.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package httpclient
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
const (
9+
surveyCollectionPathFormat = "/api/projects/%s/surveys/"
10+
surveyResourcePathFormat = "/api/projects/%s/surveys/%s/"
11+
)
12+
13+
type Survey struct {
14+
ID string `json:"id"`
15+
Name *string `json:"name,omitempty"`
16+
Description *string `json:"description,omitempty"`
17+
Type *string `json:"type,omitempty"`
18+
Schedule *string `json:"schedule,omitempty"`
19+
LinkedFlag map[string]interface{} `json:"linked_flag,omitempty"`
20+
TargetingFlag map[string]interface{} `json:"targeting_flag,omitempty"`
21+
InternalTargetingFlag map[string]interface{} `json:"internal_targeting_flag,omitempty"`
22+
Questions []interface{} `json:"questions,omitempty"`
23+
Conditions interface{} `json:"conditions,omitempty"`
24+
Appearance interface{} `json:"appearance,omitempty"`
25+
CreatedAt *string `json:"created_at,omitempty"`
26+
CreatedBy map[string]interface{} `json:"created_by,omitempty"`
27+
StartDate *string `json:"start_date,omitempty"`
28+
EndDate *string `json:"end_date,omitempty"`
29+
Archived *bool `json:"archived,omitempty"`
30+
ResponsesLimit *int64 `json:"responses_limit,omitempty"`
31+
IterationCount *int64 `json:"iteration_count,omitempty"`
32+
IterationFrequencyDays *int64 `json:"iteration_frequency_days,omitempty"`
33+
IterationStartDates []interface{} `json:"iteration_start_dates,omitempty"`
34+
CurrentIteration *int64 `json:"current_iteration,omitempty"`
35+
CurrentIterationStartDate *string `json:"current_iteration_start_date,omitempty"`
36+
ResponseSamplingStartDate *string `json:"response_sampling_start_date,omitempty"`
37+
ResponseSamplingIntervalType *string `json:"response_sampling_interval_type,omitempty"`
38+
ResponseSamplingInterval *int64 `json:"response_sampling_interval,omitempty"`
39+
ResponseSamplingLimit *int64 `json:"response_sampling_limit,omitempty"`
40+
ResponseSamplingDailyLimits interface{} `json:"response_sampling_daily_limits,omitempty"`
41+
EnablePartialResponses *bool `json:"enable_partial_responses,omitempty"`
42+
EnableIframeEmbedding *bool `json:"enable_iframe_embedding,omitempty"`
43+
Translations interface{} `json:"translations,omitempty"`
44+
FormContent interface{} `json:"form_content,omitempty"`
45+
}
46+
47+
// SurveyNullableIntegerJSONFields lists the JSON keys whose Go fields drop
48+
// `omitempty` per the SurveyRequest comment below. Both the resource and
49+
// httpclient test packages reference this slice so the list lives in one place.
50+
var SurveyNullableIntegerJSONFields = []string{
51+
"linked_flag_id",
52+
"linked_insight_id",
53+
"responses_limit",
54+
"iteration_count",
55+
"iteration_frequency_days",
56+
"response_sampling_interval",
57+
"response_sampling_limit",
58+
}
59+
60+
// SurveyRequest is the wire format for POST/PUT to the surveys endpoint.
61+
//
62+
// Fields marked nullable in the upstream OpenAPI schema (the keys in
63+
// SurveyNullableIntegerJSONFields above) deliberately omit `,omitempty` so that
64+
// a nil pointer serialises as JSON `null`. PostHog interprets `null` as "clear
65+
// this column", which is the semantic users expect when they remove the
66+
// attribute from their Terraform config. Fields that are not nullable upstream
67+
// (targeting_flag_id, archived) keep `,omitempty` so an absent value is omitted
68+
// rather than rejected.
69+
type SurveyRequest struct {
70+
Name string `json:"name"`
71+
Description *string `json:"description,omitempty"`
72+
Type string `json:"type"`
73+
Schedule *string `json:"schedule,omitempty"`
74+
LinkedFlagID *int64 `json:"linked_flag_id"`
75+
LinkedInsightID *int64 `json:"linked_insight_id"`
76+
TargetingFlagID *int64 `json:"targeting_flag_id,omitempty"`
77+
TargetingFlagFilters interface{} `json:"targeting_flag_filters,omitempty"`
78+
RemoveTargetingFlag *bool `json:"remove_targeting_flag,omitempty"`
79+
Questions []interface{} `json:"questions"`
80+
Conditions interface{} `json:"conditions,omitempty"`
81+
Appearance interface{} `json:"appearance,omitempty"`
82+
StartDate *string `json:"start_date,omitempty"`
83+
EndDate *string `json:"end_date,omitempty"`
84+
Archived *bool `json:"archived,omitempty"`
85+
ResponsesLimit *int64 `json:"responses_limit"`
86+
IterationCount *int64 `json:"iteration_count"`
87+
IterationFrequencyDays *int64 `json:"iteration_frequency_days"`
88+
ResponseSamplingStartDate *string `json:"response_sampling_start_date,omitempty"`
89+
ResponseSamplingIntervalType *string `json:"response_sampling_interval_type,omitempty"`
90+
ResponseSamplingInterval *int64 `json:"response_sampling_interval"`
91+
ResponseSamplingLimit *int64 `json:"response_sampling_limit"`
92+
ResponseSamplingDailyLimits interface{} `json:"response_sampling_daily_limits,omitempty"`
93+
EnablePartialResponses *bool `json:"enable_partial_responses,omitempty"`
94+
EnableIframeEmbedding *bool `json:"enable_iframe_embedding,omitempty"`
95+
Translations interface{} `json:"translations,omitempty"`
96+
CreateInFolder *string `json:"_create_in_folder,omitempty"`
97+
FormContent interface{} `json:"form_content,omitempty"`
98+
}
99+
100+
func (c *PosthogClient) CreateSurvey(ctx context.Context, projectID string, input SurveyRequest) (Survey, error) {
101+
path := fmt.Sprintf(surveyCollectionPathFormat, projectID)
102+
result, _, err := doPost[Survey](c, ctx, path, input)
103+
return result, err
104+
}
105+
106+
func (c *PosthogClient) GetSurvey(ctx context.Context, projectID, id string) (Survey, HTTPStatusCode, error) {
107+
path := fmt.Sprintf(surveyResourcePathFormat, projectID, id)
108+
return doGet[Survey](c, ctx, path)
109+
}
110+
111+
func (c *PosthogClient) UpdateSurvey(ctx context.Context, projectID, id string, input SurveyRequest) (Survey, HTTPStatusCode, error) {
112+
path := fmt.Sprintf(surveyResourcePathFormat, projectID, id)
113+
// PostHog's SurveyViewSet.get_serializer_class() routes only POST and PATCH
114+
// to SurveySerializerCreateUpdateOnly; PUT falls through to SurveySerializer,
115+
// whose linked_flag_id / linked_insight_id are declared with a dotted source
116+
// (`source="linked_flag.id"`) and therefore trip DRF's "writable dotted-source
117+
// fields" AssertionError on update. Use PATCH so the request hits the
118+
// write-safe serializer path.
119+
return doPatch[Survey](c, ctx, path, input)
120+
}
121+
122+
func (c *PosthogClient) DeleteSurvey(ctx context.Context, projectID, id string) (HTTPStatusCode, error) {
123+
path := fmt.Sprintf(surveyResourcePathFormat, projectID, id)
124+
return doDelete(c, ctx, path)
125+
}

0 commit comments

Comments
 (0)