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..e6b308484a01 --- /dev/null +++ b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.test.ts @@ -0,0 +1,86 @@ +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', () => { + 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) => { + // 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) + 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 26f9b40df017..702d38428998 100644 --- a/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts +++ b/products/ai_observability/frontend/feedback-view/wizard/codeExamples.ts @@ -47,7 +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' }, - { 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 ? [ @@ -56,11 +56,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,19 +66,27 @@ export function getManualCaptureExample({ surveyId = 'your-survey-id', followUpE { key: '$ai_trace_id', value: 'traceId' }, ] - const submissionIdLine = 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() +${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)} }) -${submissionIdLine}// 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)} })` @@ -90,7 +94,8 @@ ${generateProps(thumbsProps)} if (followUpEnabled) { const followUpProps: Prop[] = [ { key: '$survey_id', value: `'${surveyId}'` }, - { key: '$survey_response_1', value: "'the AI hallucinated hedgehogs everywhere'" }, + { key: '$survey_response', value: 'rating', comment: 're-send the thumbs response so it still shows' }, + { key: '$survey_response_1', value: 'followUpText', comment: "'' if the user dismissed the follow-up" }, { key: '$ai_trace_id', value: 'traceId' }, { key: '$survey_submission_id', @@ -104,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)} })`