From 9e54771a1ecf5a93571bc0d186e1b922c61fc32b Mon Sep 17 00:00:00 2001 From: Phill <14913130+phillram@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:24:12 -0700 Subject: [PATCH 1/3] fix(aio): stop the wizard sample from dropping feedback answers The generated manual-capture snippet split a submission's answers across two `survey sent` events: the thumbs response only on the first, the follow-up text only on the second. PostHog reads a submission's answers from the completed event, so the thumbs rating was dropped from the response and the question breakdown. The sample now derives the rating and the completion flag from one `rating` variable. The completed event re-sends the rating, and a thumbs up completes the submission on its own event, because the survey branches a thumbs up straight to End and never sends a follow-up. Re-lands PostHog/posthog#76115 with the two issues raised in review on it. Generated-By: PostHog Desktop Task-Id: a05b8ee3-88a3-42b0-bc16-07a58ca44d30 --- .../feedback-view/wizard/codeExamples.test.ts | 14 ++++++++++++ .../feedback-view/wizard/codeExamples.ts | 22 ++++++++++++------- 2 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts diff --git a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts new file mode 100644 index 000000000000..dec35c16f0ef --- /dev/null +++ b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts @@ -0,0 +1,14 @@ +import { getManualCaptureExample } from './codeExamples' + +describe('getManualCaptureExample', () => { + it('does not mark the rating event incomplete for a rating that ends the survey', () => { + const thumbsEvent = getManualCaptureExample({ followUpEnabled: true }) + .split("posthog.capture('survey sent', {")[1] + .split('})')[0] + + // The wizard branches a thumbs up straight to End, so it never sends a follow-up event. + // A literal `false` here leaves every thumbs up submission without a completed event, + // which hides it from surveys that do not enable partial responses. + expect(thumbsEvent).not.toMatch(/\$survey_completed: false\b/) + }) +}) diff --git a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts index 26f9b40df017..c7649552733d 100644 --- a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts +++ b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts @@ -47,7 +47,9 @@ const generateProps = (props: Prop[], indent = 2): string => { export function getManualCaptureExample({ surveyId = 'your-survey-id', followUpEnabled }: CodeExampleParams): string { const thumbsProps: Prop[] = [ { key: '$survey_id', value: `'${surveyId}'`, comment: 'ID for the survey you just created' }, - { key: '$survey_response', value: '1', comment: '1 = thumbs up, 2 = thumbs down' }, + followUpEnabled + ? { key: '$survey_response', value: 'rating' } + : { key: '$survey_response', value: '1', comment: '1 = thumbs up, 2 = thumbs down' }, { key: '$ai_trace_id', value: 'traceId', comment: 'your generated trace ID' }, ...(followUpEnabled ? [ @@ -56,11 +58,7 @@ export function getManualCaptureExample({ surveyId = 'your-survey-id', followUpE value: 'submissionId', comment: 'unique ID to link thumbs + follow-up', }, - { - key: '$survey_completed', - value: 'true', - comment: 'or false if there is negative feedback followup', - }, + { key: '$survey_completed', value: '!expectsFollowUp' }, ] : []), ] @@ -70,10 +68,17 @@ export function getManualCaptureExample({ surveyId = 'your-survey-id', followUpE { key: '$ai_trace_id', value: 'traceId' }, ] - const submissionIdLine = followUpEnabled + // The survey branches to the follow-up on a thumbs down and ends on a thumbs up, so the + // sample derives both the rating and the completion flag from one variable. Hard-coding + // `$survey_completed: false` would leave every thumbs up without a completed event. + const followUpPreamble = followUpEnabled ? `// Generate a unique ID to link \`survey sent\` events into a single user feedback event const submissionId = crypto.randomUUID() +const rating = 2 // 1 = thumbs up, 2 = thumbs down +// Only a thumbs down opens the follow-up, so a thumbs up completes the submission here +const expectsFollowUp = rating === 2 + ` : '' @@ -82,7 +87,7 @@ posthog.capture('survey shown', { ${generateProps(surveyShownProps)} }) -${submissionIdLine}// When user clicks thumbs up/down, send a survey event +${followUpPreamble}// When user clicks thumbs up/down, send a survey event posthog.capture('survey sent', { ${generateProps(thumbsProps)} })` @@ -90,6 +95,7 @@ ${generateProps(thumbsProps)} if (followUpEnabled) { const followUpProps: Prop[] = [ { key: '$survey_id', value: `'${surveyId}'` }, + { key: '$survey_response', value: 'rating', comment: 're-send the thumbs response so it still shows' }, { key: '$survey_response_1', value: "'the AI hallucinated hedgehogs everywhere'" }, { key: '$ai_trace_id', value: 'traceId' }, { From 557a9e71b16b18699f00262a2e3abf300b61d38a Mon Sep 17 00:00:00 2001 From: Phill <14913130+phillram@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:48:07 -0700 Subject: [PATCH 2/3] fix(aio): keep the wizard sample's rating when the follow-up is dismissed The sample only sent its completing event when the user submitted follow-up text. A user who rated thumbs down and then closed the follow-up left one incomplete event behind, so the whole submission dropped out of Results. The follow-up event now fires when the follow-up closes, whether the user answered or dismissed it, and carries an empty answer in the dismissal case. Both snippet variants now read the rating off the click instead of hard-coding it, so a verbatim copy no longer reports the same thumb every time. The test runs the generated sample against a stub client and asserts the events a pasted integration would send, covering the thumbs up, answered follow-up, and dismissed follow-up paths. Generated-By: PostHog Desktop Task-Id: 4928fcb4-50f8-4c7e-ab85-d1f3964e8e03 --- .../feedback-view/wizard/codeExamples.test.ts | 88 +++++++++++++++++-- .../feedback-view/wizard/codeExamples.ts | 24 ++--- 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts index dec35c16f0ef..5adfccc11c85 100644 --- a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts +++ b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts @@ -1,14 +1,84 @@ import { getManualCaptureExample } from './codeExamples' +interface SamplePath { + followUpEnabled?: boolean + clickedThumbsUp: boolean + followUpText?: string +} + +// Run the generated sample against a stub, so the assertions read the events a pasted +// integration sends rather than the text of the snippet. +function runSample({ + followUpEnabled = true, + clickedThumbsUp, + followUpText = '', +}: SamplePath): Record[] { + const sent: Record[] = [] + const posthog = { + capture: (event: string, properties: Record): void => { + if (event === 'survey sent') { + sent.push(properties) + } + }, + } + + const sample = new Function( + 'posthog', + 'crypto', + 'traceId', + 'clickedThumbsUp', + 'followUpText', + getManualCaptureExample({ surveyId: 'survey-1', followUpEnabled }) + ) + sample(posthog, { randomUUID: () => 'submission-1' }, 'trace-1', clickedThumbsUp, followUpText) + + return sent +} + describe('getManualCaptureExample', () => { - it('does not mark the rating event incomplete for a rating that ends the survey', () => { - const thumbsEvent = getManualCaptureExample({ followUpEnabled: true }) - .split("posthog.capture('survey sent', {")[1] - .split('})')[0] - - // The wizard branches a thumbs up straight to End, so it never sends a follow-up event. - // A literal `false` here leaves every thumbs up submission without a completed event, - // which hides it from surveys that do not enable partial responses. - expect(thumbsEvent).not.toMatch(/\$survey_completed: false\b/) + describe('with a follow-up question', () => { + // Results keeps one row per `$survey_submission_id` and drops a submission that has no + // completed event. So every path has to end on exactly one completed event, and that + // event has to carry every answer given so far. + it.each([ + { + path: 'a thumbs up, which branches straight to the end', + clickedThumbsUp: true, + followUpText: '', + eventsSent: 1, + rating: 1, + answer: undefined, + }, + { + path: 'a thumbs down with follow-up text', + clickedThumbsUp: false, + followUpText: 'the AI hallucinated hedgehogs everywhere', + eventsSent: 2, + rating: 2, + answer: 'the AI hallucinated hedgehogs everywhere', + }, + { + path: 'a thumbs down whose follow-up is dismissed', + clickedThumbsUp: false, + followUpText: '', + eventsSent: 2, + rating: 2, + answer: '', + }, + ])('completes $path with every answer on the completed event', (testCase) => { + const sent = runSample(testCase).slice(0, testCase.eventsSent) + + const completed = sent.filter((event) => event.$survey_completed === true) + expect(completed).toHaveLength(1) + expect(completed[0].$survey_response).toBe(testCase.rating) + expect(completed[0].$survey_response_1).toBe(testCase.answer) + expect(new Set(sent.map((event) => event.$survey_submission_id)).size).toBe(1) + }) + }) + + it('reads the rating off the click when there is no follow-up', () => { + const [sent] = runSample({ followUpEnabled: false, clickedThumbsUp: false }) + + expect(sent.$survey_response).toBe(2) }) }) diff --git a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts index c7649552733d..702d38428998 100644 --- a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts +++ b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts @@ -47,9 +47,7 @@ const generateProps = (props: Prop[], indent = 2): string => { export function getManualCaptureExample({ surveyId = 'your-survey-id', followUpEnabled }: CodeExampleParams): string { const thumbsProps: Prop[] = [ { key: '$survey_id', value: `'${surveyId}'`, comment: 'ID for the survey you just created' }, - followUpEnabled - ? { key: '$survey_response', value: 'rating' } - : { key: '$survey_response', value: '1', comment: '1 = thumbs up, 2 = thumbs down' }, + { key: '$survey_response', value: 'rating' }, { key: '$ai_trace_id', value: 'traceId', comment: 'your generated trace ID' }, ...(followUpEnabled ? [ @@ -68,26 +66,27 @@ export function getManualCaptureExample({ surveyId = 'your-survey-id', followUpE { key: '$ai_trace_id', value: 'traceId' }, ] - // The survey branches to the follow-up on a thumbs down and ends on a thumbs up, so the - // sample derives both the rating and the completion flag from one variable. Hard-coding - // `$survey_completed: false` would leave every thumbs up without a completed event. - const followUpPreamble = followUpEnabled + const ratingLine = 'const rating = clickedThumbsUp ? 1 : 2 // 1 = thumbs up, 2 = thumbs down' + + const preamble = followUpEnabled ? `// Generate a unique ID to link \`survey sent\` events into a single user feedback event const submissionId = crypto.randomUUID() -const rating = 2 // 1 = thumbs up, 2 = thumbs down +${ratingLine} // Only a thumbs down opens the follow-up, so a thumbs up completes the submission here const expectsFollowUp = rating === 2 ` - : '' + : `${ratingLine} + +` const base = `// (Optional) Track when the survey is shown to the user posthog.capture('survey shown', { ${generateProps(surveyShownProps)} }) -${followUpPreamble}// When user clicks thumbs up/down, send a survey event +${preamble}// When user clicks thumbs up/down, send a survey event posthog.capture('survey sent', { ${generateProps(thumbsProps)} })` @@ -96,7 +95,7 @@ ${generateProps(thumbsProps)} const followUpProps: Prop[] = [ { key: '$survey_id', value: `'${surveyId}'` }, { key: '$survey_response', value: 'rating', comment: 're-send the thumbs response so it still shows' }, - { key: '$survey_response_1', value: "'the AI hallucinated hedgehogs everywhere'" }, + { key: '$survey_response_1', value: 'followUpText', comment: "'' if the user dismissed the follow-up" }, { key: '$ai_trace_id', value: 'traceId' }, { key: '$survey_submission_id', @@ -110,7 +109,8 @@ ${generateProps(thumbsProps)} base + ` -// If the user submitted follow-up text after thumbs down: +// When the follow-up closes, send the answers so far. +// Send it even on dismissal, or PostHog drops the submission and the thumbs rating with it. posthog.capture('survey sent', { ${generateProps(followUpProps)} })` From f13d84df3aaea63ba50dace69fa1e7150c6686fd Mon Sep 17 00:00:00 2001 From: Phill <14913130+phillram@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:50:38 -0700 Subject: [PATCH 3/3] chore(aio): explain why the sample run is trimmed to one path Generated-By: PostHog Desktop Task-Id: 4928fcb4-50f8-4c7e-ab85-d1f3964e8e03 --- .../frontend/feedback-view/wizard/codeExamples.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts index 5adfccc11c85..e6b308484a01 100644 --- a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts +++ b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts @@ -66,6 +66,8 @@ describe('getManualCaptureExample', () => { answer: '', }, ])('completes $path with every answer on the completed event', (testCase) => { + // The sample wires its second capture to the follow-up closing, so a straight run + // reaches it on every path. `eventsSent` trims the run back to one real path. const sent = runSample(testCase).slice(0, testCase.eventsSent) const completed = sent.filter((event) => event.$survey_completed === true)